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:
parent
1c261675ad
commit
d058ef2b6c
|
@ -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'],
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
*
|
*
|
||||||
|
|
|
@ -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())
|
||||||
|
|
Loading…
Reference in New Issue