Make it possible to wipe all tokens/devices of a user

Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
This commit is contained in:
Christoph Wurst 2019-07-03 10:10:56 +02:00 committed by Roeland Jago Douma
parent 1c261675ad
commit d058ef2b6c
No known key found for this signature in database
GPG Key ID: F941078878347C0C
10 changed files with 171 additions and 31 deletions

View File

@ -50,6 +50,7 @@ return [
['root' => '/cloud', 'name' => 'Users#getCurrentUser', 'url' => '/user', 'verb' => 'GET'], ['root' => '/cloud', 'name' => 'Users#getCurrentUser', 'url' => '/user', 'verb' => 'GET'],
['root' => '/cloud', 'name' => 'Users#getEditableFields', 'url' => '/user/fields', 'verb' => 'GET'], ['root' => '/cloud', 'name' => 'Users#getEditableFields', 'url' => '/user/fields', 'verb' => 'GET'],
['root' => '/cloud', 'name' => 'Users#editUser', 'url' => '/users/{userId}', 'verb' => 'PUT'], ['root' => '/cloud', 'name' => 'Users#editUser', 'url' => '/users/{userId}', 'verb' => 'PUT'],
['root' => '/cloud', 'name' => 'Users#wipeUserDevices', 'url' => '/users/{userId}/wipe', 'verb' => 'POST'],
['root' => '/cloud', 'name' => 'Users#deleteUser', 'url' => '/users/{userId}', 'verb' => 'DELETE'], ['root' => '/cloud', 'name' => 'Users#deleteUser', 'url' => '/users/{userId}', 'verb' => 'DELETE'],
['root' => '/cloud', 'name' => 'Users#enableUser', 'url' => '/users/{userId}/enable', 'verb' => 'PUT'], ['root' => '/cloud', 'name' => 'Users#enableUser', 'url' => '/users/{userId}/enable', 'verb' => 'PUT'],
['root' => '/cloud', 'name' => 'Users#disableUser', 'url' => '/users/{userId}/disable', 'verb' => 'PUT'], ['root' => '/cloud', 'name' => 'Users#disableUser', 'url' => '/users/{userId}/disable', 'verb' => 'PUT'],

View File

@ -34,6 +34,7 @@ declare(strict_types=1);
namespace OCA\Provisioning_API\Controller; namespace OCA\Provisioning_API\Controller;
use OC\Accounts\AccountManager; use OC\Accounts\AccountManager;
use OC\Authentication\Token\RemoteWipe;
use OC\HintException; use OC\HintException;
use OC\Settings\Mailer\NewUserMailHelper; use OC\Settings\Mailer\NewUserMailHelper;
use OCA\Provisioning_API\FederatedFileSharingFactory; use OCA\Provisioning_API\FederatedFileSharingFactory;
@ -46,6 +47,7 @@ use OCP\IGroup;
use OCP\IGroupManager; use OCP\IGroupManager;
use OCP\ILogger; use OCP\ILogger;
use OCP\IRequest; use OCP\IRequest;
use OCP\IUser;
use OCP\IUserManager; use OCP\IUserManager;
use OCP\IUserSession; use OCP\IUserSession;
use OCP\L10N\IFactory; use OCP\L10N\IFactory;
@ -65,6 +67,8 @@ class UsersController extends AUserData {
private $federatedFileSharingFactory; private $federatedFileSharingFactory;
/** @var ISecureRandom */ /** @var ISecureRandom */
private $secureRandom; private $secureRandom;
/** @var RemoteWipe */
private $remoteWipe;
/** /**
* @param string $appName * @param string $appName
@ -93,7 +97,8 @@ class UsersController extends AUserData {
IFactory $l10nFactory, IFactory $l10nFactory,
NewUserMailHelper $newUserMailHelper, NewUserMailHelper $newUserMailHelper,
FederatedFileSharingFactory $federatedFileSharingFactory, FederatedFileSharingFactory $federatedFileSharingFactory,
ISecureRandom $secureRandom) { ISecureRandom $secureRandom,
RemoteWipe $remoteWipe) {
parent::__construct($appName, parent::__construct($appName,
$request, $request,
$userManager, $userManager,
@ -108,6 +113,7 @@ class UsersController extends AUserData {
$this->newUserMailHelper = $newUserMailHelper; $this->newUserMailHelper = $newUserMailHelper;
$this->federatedFileSharingFactory = $federatedFileSharingFactory; $this->federatedFileSharingFactory = $federatedFileSharingFactory;
$this->secureRandom = $secureRandom; $this->secureRandom = $secureRandom;
$this->remoteWipe = $remoteWipe;
} }
/** /**
@ -587,6 +593,37 @@ class UsersController extends AUserData {
return new DataResponse(); return new DataResponse();
} }
/**
* @PasswordConfirmationRequired
* @NoAdminRequired
*
* @param string $userId
*
* @return DataResponse
*
* @throws OCSException
*/
public function wipeUserDevices(string $userId): DataResponse {
/** @var IUser $currentLoggedInUser */
$currentLoggedInUser = $this->userSession->getUser();
$targetUser = $this->userManager->get($userId);
if ($targetUser === null || $targetUser->getUID() === $currentLoggedInUser->getUID()) {
throw new OCSException('', 101);
}
// If not permitted
$subAdminManager = $this->groupManager->getSubAdmin();
if (!$this->groupManager->isAdmin($currentLoggedInUser->getUID()) && !$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)) {
throw new OCSException('', \OCP\API::RESPOND_UNAUTHORISED);
}
$this->remoteWipe->markAllTokensForWipe($targetUser);
return new DataResponse();
}
/** /**
* @PasswordConfirmationRequired * @PasswordConfirmationRequired
* @NoAdminRequired * @NoAdminRequired

View File

@ -25,19 +25,15 @@ declare(strict_types=1);
namespace OC\Authentication\Token; namespace OC\Authentication\Token;
use BadMethodCallException; use function array_filter;
use OC\Authentication\Events\RemoteWipeFinished; use OC\Authentication\Events\RemoteWipeFinished;
use OC\Authentication\Events\RemoteWipeStarted; use OC\Authentication\Events\RemoteWipeStarted;
use OC\Authentication\Exceptions\ExpiredTokenException;
use OC\Authentication\Exceptions\InvalidTokenException; use OC\Authentication\Exceptions\InvalidTokenException;
use OC\Authentication\Exceptions\WipeTokenException; use OC\Authentication\Exceptions\WipeTokenException;
use OCP\Activity\IEvent;
use OCP\Activity\IManager as IActivityManager;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\EventDispatcher\IEventDispatcher; use OCP\EventDispatcher\IEventDispatcher;
use OCP\ILogger; use OCP\ILogger;
use OCP\IUser; use OCP\IUser;
use OCP\Notification\IManager as INotificationManager;
use Symfony\Component\EventDispatcher\EventDispatcher;
class RemoteWipe { class RemoteWipe {
@ -58,6 +54,15 @@ class RemoteWipe {
$this->logger = $logger; $this->logger = $logger;
} }
/**
* @param int $id
*
* @return bool
*
* @throws InvalidTokenException
* @throws WipeTokenException
* @throws ExpiredTokenException
*/
public function markTokenForWipe(int $id): bool { public function markTokenForWipe(int $id): bool {
$token = $this->tokenProvider->getTokenById($id); $token = $this->tokenProvider->getTokenById($id);
@ -71,6 +76,31 @@ class RemoteWipe {
return true; return true;
} }
/**
* @param IUser $user
*
* @return bool true if any tokens have been marked for remote wipe
*/
public function markAllTokensForWipe(IUser $user): bool {
$tokens = $this->tokenProvider->getTokenByUser($user->getUID());
/** @var IWipeableToken[] $wipeable */
$wipeable = array_filter($tokens, function (IToken $token) {
return $token instanceof IWipeableToken;
});
if (empty($wipeable)) {
return false;
}
foreach ($wipeable as $token) {
$token->wipe();
$this->tokenProvider->updateToken($token);
}
return true;
}
/** /**
* @param string $token * @param string $token
* *

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -23,10 +23,10 @@
<template> <template>
<!-- Obfuscated user: Logged in user does not have permissions to see all of the data --> <!-- Obfuscated user: Logged in user does not have permissions to see all of the data -->
<div class="row" v-if="Object.keys(user).length ===1" :data-id="user.id"> <div class="row" v-if="Object.keys(user).length ===1" :data-id="user.id">
<div class="avatar" :class="{'icon-loading-small': loading.delete || loading.disable}"> <div class="avatar" :class="{'icon-loading-small': loading.delete || loading.disable || loading.wipe}">
<img alt="" width="32" height="32" :src="generateAvatar(user.id, 32)" <img alt="" width="32" height="32" :src="generateAvatar(user.id, 32)"
:srcset="generateAvatar(user.id, 64)+' 2x, '+generateAvatar(user.id, 128)+' 4x'" :srcset="generateAvatar(user.id, 64)+' 2x, '+generateAvatar(user.id, 128)+' 4x'"
v-if="!loading.delete && !loading.disable"> v-if="!loading.delete && !loading.disable && !loading.wipe">
</div> </div>
<div class="name">{{user.id}}</div> <div class="name">{{user.id}}</div>
<div class="obfuscated">{{t('settings','You do not have permissions to see the details of this user')}}</div> <div class="obfuscated">{{t('settings','You do not have permissions to see the details of this user')}}</div>
@ -34,10 +34,10 @@
<!-- User full data --> <!-- User full data -->
<div class="row" v-else :class="{'disabled': loading.delete || loading.disable}" :data-id="user.id"> <div class="row" v-else :class="{'disabled': loading.delete || loading.disable}" :data-id="user.id">
<div class="avatar" :class="{'icon-loading-small': loading.delete || loading.disable}"> <div class="avatar" :class="{'icon-loading-small': loading.delete || loading.disable || loading.wipe}">
<img alt="" width="32" height="32" :src="generateAvatar(user.id, 32)" <img alt="" width="32" height="32" :src="generateAvatar(user.id, 32)"
:srcset="generateAvatar(user.id, 64)+' 2x, '+generateAvatar(user.id, 128)+' 4x'" :srcset="generateAvatar(user.id, 64)+' 2x, '+generateAvatar(user.id, 128)+' 4x'"
v-if="!loading.delete && !loading.disable"> v-if="!loading.delete && !loading.disable && !loading.wipe">
</div> </div>
<!-- dirty hack to ellipsis on two lines --> <!-- dirty hack to ellipsis on two lines -->
<div class="name">{{user.id}}</div> <div class="name">{{user.id}}</div>
@ -165,22 +165,31 @@ export default {
quota: false, quota: false,
delete: false, delete: false,
disable: false, disable: false,
languages: false languages: false,
wipe: false,
} }
} }
}, },
computed: { computed: {
/* USER POPOVERMENU ACTIONS */ /* USER POPOVERMENU ACTIONS */
userActions() { userActions() {
let actions = [{ let actions = [
icon: 'icon-delete', {
text: t('settings','Delete user'), icon: 'icon-delete',
action: this.deleteUser text: t('settings', 'Delete user'),
},{ action: this.deleteUser,
icon: this.user.enabled ? 'icon-close' : 'icon-add', },
text: this.user.enabled ? t('settings','Disable user') : t('settings','Enable user'), {
action: this.enableDisableUser icon: 'icon-delete',
}]; text: t('settings', 'Wipe all devices'),
action: this.wipeUserDevices,
},
{
icon: this.user.enabled ? 'icon-close' : 'icon-add',
text: this.user.enabled ? t('settings', 'Disable user') : t('settings', 'Enable user'),
action: this.enableDisableUser,
},
];
if (this.user.email !== null && this.user.email !== '') { if (this.user.email !== null && this.user.email !== '') {
actions.push({ actions.push({
icon: 'icon-mail', icon: 'icon-mail',
@ -308,6 +317,17 @@ export default {
return names.slice(2,).join(', '); return names.slice(2,).join(', ');
}, },
wipeUserDevices() {
this.loading.wipe = true;
this.loading.all = true;
let userid = this.user.id;
return this.$store.dispatch('wipeUserDevices', userid)
.then(() => {
this.loading.wipe = false
this.loading.all = false
});
},
deleteUser() { deleteUser() {
this.loading.delete = true; this.loading.delete = true;
this.loading.all = true; this.loading.all = true;

View File

@ -397,6 +397,20 @@ const actions = {
}).catch((error) => context.commit('API_FAILURE', { userid, error })); }).catch((error) => context.commit('API_FAILURE', { userid, error }));
}, },
/**
* Mark all user devices for remote wipe
*
* @param {Object} context
* @param {string} userid User id
* @returns {Promise}
*/
wipeUserDevices(context, userid) {
return api.requireAdmin().then((response) => {
return api.post(OC.linkToOCS(`cloud/users/${userid}/wipe`, 2))
.catch((error) => {throw error;});
}).catch((error) => context.commit('API_FAILURE', { userid, error }));
},
/** /**
* Delete a user * Delete a user
* *

View File

@ -33,6 +33,7 @@ use OC\Authentication\Token\IWipeableToken;
use OC\Authentication\Token\RemoteWipe; use OC\Authentication\Token\RemoteWipe;
use OCP\EventDispatcher\IEventDispatcher; use OCP\EventDispatcher\IEventDispatcher;
use OCP\ILogger; use OCP\ILogger;
use OCP\IUser;
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase; use Test\TestCase;
@ -93,6 +94,43 @@ class RemoteWipeTest extends TestCase {
$this->assertTrue($result); $this->assertTrue($result);
} }
public function testMarkAllTokensForWipeNoWipeableToken(): void {
/** @var IUser|MockObject $user */
$user = $this->createMock(IUser::class);
$user->method('getUID')->willReturn('user123');
$token1 = $this->createMock(IToken::class);
$token2 = $this->createMock(IToken::class);
$this->tokenProvider->expects($this->once())
->method('getTokenByUser')
->with('user123')
->willReturn([$token1, $token2]);
$result = $this->remoteWipe->markAllTokensForWipe($user);
$this->assertFalse($result);
}
public function testMarkAllTokensForWipe(): void {
/** @var IUser|MockObject $user */
$user = $this->createMock(IUser::class);
$user->method('getUID')->willReturn('user123');
$token1 = $this->createMock(IToken::class);
$token2 = $this->createMock(IWipeableToken::class);
$this->tokenProvider->expects($this->once())
->method('getTokenByUser')
->with('user123')
->willReturn([$token1, $token2]);
$token2->expects($this->once())
->method('wipe');
$this->tokenProvider->expects($this->once())
->method('updateToken')
->with($token2);
$result = $this->remoteWipe->markAllTokensForWipe($user);
$this->assertTrue($result);
}
public function testStartWipingNotAWipeToken() { public function testStartWipingNotAWipeToken() {
$token = $this->createMock(IToken::class); $token = $this->createMock(IToken::class);
$this->tokenProvider->expects($this->once()) $this->tokenProvider->expects($this->once())