Generate a notification to generate backup codes if you enable an other 2FA provider but backup codes are not yet generated. * Add event listner * Insert background job * Background job tests and emits notification every 2 weeks * If the backup codes are generated the next run will remove the job Signed-off-by: Roeland Jago Douma <roeland@famdouma.nl>pull/11462/head
parent
a95154642d
commit
956fe1b867
@ -0,0 +1,92 @@ |
||||
<?php |
||||
declare(strict_types=1); |
||||
/** |
||||
* @copyright Copyright (c) 2018, Roeland Jago Douma <roeland@famdouma.nl> |
||||
* |
||||
* @author Roeland Jago Douma <roeland@famdouma.nl> |
||||
* |
||||
* @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 OCA\TwoFactorBackupCodes\BackgroundJob; |
||||
|
||||
use OC\BackgroundJob\TimedJob; |
||||
use OCP\AppFramework\Utility\ITimeFactory; |
||||
use OCP\Authentication\TwoFactorAuth\IRegistry; |
||||
use OCP\BackgroundJob\IJobList; |
||||
use OCP\IUserManager; |
||||
use OCP\Notification\IManager; |
||||
|
||||
class RememberBackupCodesJob extends TimedJob { |
||||
|
||||
/** @var IRegistry */ |
||||
private $registry; |
||||
|
||||
/** @var IUserManager */ |
||||
private $userManager; |
||||
|
||||
/** @var ITimeFactory */ |
||||
private $time; |
||||
|
||||
/** @var IManager */ |
||||
private $notificationManager; |
||||
|
||||
/** @var IJobList */ |
||||
private $jobList; |
||||
|
||||
public function __construct(IRegistry $registry, |
||||
IUserManager $userManager, |
||||
ITimeFactory $timeFactory, |
||||
IManager $notificationManager, |
||||
IJobList $jobList) { |
||||
$this->registry = $registry; |
||||
$this->userManager = $userManager; |
||||
$this->time = $timeFactory; |
||||
$this->notificationManager = $notificationManager; |
||||
$this->jobList = $jobList; |
||||
|
||||
$this->setInterval(60*60*24*14); |
||||
} |
||||
|
||||
protected function run($argument) { |
||||
$uid = $argument['uid']; |
||||
$user = $this->userManager->get($uid); |
||||
|
||||
if ($user === null) { |
||||
// We can't run with an invalid user |
||||
return; |
||||
} |
||||
|
||||
$providers = $this->registry->getProviderStates($user); |
||||
if (isset($providers['backup_codes']) && $providers['backup_codes'] === true) { |
||||
// Backup codes already generated lets remove this job |
||||
$this->jobList->remove(self::class, $argument); |
||||
return; |
||||
} |
||||
|
||||
$date = new \DateTime(); |
||||
$date->setTimestamp($this->time->getTime()); |
||||
|
||||
$notification = $this->notificationManager->createNotification(); |
||||
$notification->setApp('twofactor_backupcodes') |
||||
->setUser($user->getUID()) |
||||
->setDateTime($date) |
||||
->setObject('create', 'codes') |
||||
->setSubject('create_backupcodes'); |
||||
$this->notificationManager->notify($notification); |
||||
} |
||||
} |
@ -0,0 +1,61 @@ |
||||
<?php |
||||
declare(strict_types=1); |
||||
/** |
||||
* @copyright Copyright (c) 2018, Roeland Jago Douma <roeland@famdouma.nl> |
||||
* |
||||
* @author Roeland Jago Douma <roeland@famdouma.nl> |
||||
* |
||||
* @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 OCA\TwoFactorBackupCodes\Listener; |
||||
|
||||
use OCA\TwoFactorBackupCodes\BackgroundJob\RememberBackupCodesJob; |
||||
use OCP\Authentication\TwoFactorAuth\IRegistry; |
||||
use OCP\Authentication\TwoFactorAuth\RegistryEvent; |
||||
use OCP\BackgroundJob\IJobList; |
||||
use Symfony\Component\EventDispatcher\Event; |
||||
|
||||
class ProviderEnabled implements IListener { |
||||
|
||||
/** @var IRegistry */ |
||||
private $registry; |
||||
|
||||
/** @var IJobList */ |
||||
private $jobList; |
||||
|
||||
public function __construct(IRegistry $registry, |
||||
IJobList $jobList) { |
||||
$this->registry = $registry; |
||||
$this->jobList = $jobList; |
||||
} |
||||
|
||||
public function handle(Event $event) { |
||||
if (!($event instanceof RegistryEvent)) { |
||||
return; |
||||
} |
||||
|
||||
$providers = $this->registry->getProviderStates($event->getUser()); |
||||
if (isset($providers['backup_codes']) && $providers['backup_codes'] === true) { |
||||
// Backup codes already generated nothing to do here |
||||
return; |
||||
} |
||||
|
||||
$this->jobList->add(RememberBackupCodesJob::class, ['uid' => $event->getUser()->getUID()]); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,64 @@ |
||||
<?php |
||||
declare(strict_types=1); |
||||
/** |
||||
* @copyright Copyright (c) 2018, Roeland Jago Douma <roeland@famdouma.nl> |
||||
* |
||||
* @author Roeland Jago Douma <roeland@famdouma.nl> |
||||
* |
||||
* @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 OCA\TwoFactorBackupCodes\Notifications; |
||||
|
||||
use OCP\L10N\IFactory; |
||||
use OCP\Notification\INotification; |
||||
use OCP\Notification\INotifier; |
||||
|
||||
class Notifier implements INotifier { |
||||
|
||||
/** @var IFactory */ |
||||
private $factory; |
||||
|
||||
public function __construct(IFactory $factory) { |
||||
$this->factory = $factory; |
||||
} |
||||
|
||||
public function prepare(INotification $notification, $languageCode) { |
||||
if ($notification->getApp() !== 'twofactor_backupcodes') { |
||||
// Not my app => throw |
||||
throw new \InvalidArgumentException(); |
||||
} |
||||
|
||||
// Read the language from the notification |
||||
$l = $this->factory->get('twofactor_backupcodes', $languageCode); |
||||
|
||||
switch ($notification->getSubject()) { |
||||
case 'create_backupcodes': |
||||
$notification->setParsedSubject( |
||||
$l->t('Generate backup codes') |
||||
)->setParsedMessage( |
||||
$l->t('You have enabled two-factor authentication but have not yet generated backup codes. Be sure to do this in case you lose access to your second factor.') |
||||
); |
||||
return $notification; |
||||
|
||||
default: |
||||
// Unknown subject => Unknown notification => throw |
||||
throw new \InvalidArgumentException(); |
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,153 @@ |
||||
<?php |
||||
declare(strict_types=1); |
||||
/** |
||||
* @copyright Copyright (c) 2018, Roeland Jago Douma <roeland@famdouma.nl> |
||||
* |
||||
* @author Roeland Jago Douma <roeland@famdouma.nl> |
||||
* |
||||
* @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 OCA\TwoFactorBackupCodes\Tests\Unit\BackgroundJob; |
||||
|
||||
use OCA\TwoFactorBackupCodes\BackgroundJob\RememberBackupCodesJob; |
||||
use OCP\AppFramework\Utility\ITimeFactory; |
||||
use OCP\Authentication\TwoFactorAuth\IRegistry; |
||||
use OCP\BackgroundJob\IJobList; |
||||
use OCP\IUser; |
||||
use OCP\IUserManager; |
||||
use OCP\Notification\IManager; |
||||
use OCP\Notification\INotification; |
||||
use Test\TestCase; |
||||
|
||||
class RememberBackupCodesJobTest extends TestCase { |
||||
|
||||
/** @var IRegistry|\PHPUnit\Framework\MockObject\MockObject */ |
||||
private $registry; |
||||
|
||||
/** @var IUserManager|\PHPUnit\Framework\MockObject\MockObject */ |
||||
private $userManager; |
||||
|
||||
/** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */ |
||||
private $time; |
||||
|
||||
/** @var IManager|\PHPUnit\Framework\MockObject\MockObject */ |
||||
private $notificationManager; |
||||
|
||||
/** @var IJobList|\PHPUnit\Framework\MockObject\MockObject */ |
||||
private $jobList; |
||||
|
||||
/** @var RememberBackupCodesJob */ |
||||
private $job; |
||||
|
||||
public function setUp() { |
||||
parent::setUp(); |
||||
|
||||
$this->registry = $this->createMock(IRegistry::class); |
||||
$this->userManager = $this->createMock(IUserManager::class); |
||||
$this->time = $this->createMock(ITimeFactory::class); |
||||
$this->time->method('getTime') |
||||
->willReturn(10000000); |
||||
$this->notificationManager = $this->createMock(IManager::class); |
||||
$this->jobList = $this->createMock(IJobList::class); |
||||
|
||||
$this->job = new RememberBackupCodesJob( |
||||
$this->registry, |
||||
$this->userManager, |
||||
$this->time, |
||||
$this->notificationManager, |
||||
$this->jobList |
||||
); |
||||
} |
||||
|
||||
public function testInvalidUID() { |
||||
$this->userManager->method('get') |
||||
->with('invalidUID') |
||||
->willReturn(null); |
||||
|
||||
$this->notificationManager->expects($this->never()) |
||||
->method($this->anything()); |
||||
$this->jobList->expects($this->never()) |
||||
->method($this->anything()); |
||||
|
||||
$this->invokePrivate($this->job, 'run', [['uid' => 'invalidUID']]); |
||||
} |
||||
|
||||
public function testBackupCodesGenerated() { |
||||
$user = $this->createMock(IUser::class); |
||||
$user->method('getUID') |
||||
->willReturn('validUID'); |
||||
$this->userManager->method('get') |
||||
->with('validUID') |
||||
->willReturn($user); |
||||
|
||||
$this->registry->method('getProviderStates') |
||||
->with($user) |
||||
->willReturn([ |
||||
'backup_codes' => true |
||||
]); |
||||
|
||||
$this->jobList->expects($this->once()) |
||||
->method('remove') |
||||
->with( |
||||
RememberBackupCodesJob::class, |
||||
['uid' => 'validUID'] |
||||
); |
||||
|
||||
$this->notificationManager->expects($this->never()) |
||||
->method($this->anything()); |
||||
|
||||
$this->invokePrivate($this->job, 'run', [['uid' => 'validUID']]); |
||||
} |
||||
|
||||
public function testNotificationSend() { |
||||
$user = $this->createMock(IUser::class); |
||||
$user->method('getUID') |
||||
->willReturn('validUID'); |
||||
$this->userManager->method('get') |
||||
->with('validUID') |
||||
->willReturn($user); |
||||
|
||||
$this->registry->method('getProviderStates') |
||||
->with($user) |
||||
->willReturn([ |
||||
'backup_codes' => false |
||||
]); |
||||
|
||||
$this->jobList->expects($this->never()) |
||||
->method($this->anything()); |
||||
|
||||
$date = new \DateTime(); |
||||
$date->setTimestamp($this->time->getTime()); |
||||
|
||||
$this->notificationManager->method('createNotification') |
||||
->willReturn(\OC::$server->query(IManager::class)->createNotification()); |
||||
|
||||
$this->notificationManager->expects($this->once()) |
||||
->method('notify') |
||||
->with($this->callback(function (INotification $n) { |
||||
return $n->getApp() === 'twofactor_backupcodes' && |
||||
$n->getUser() === 'validUID' && |
||||
$n->getDateTime()->getTimestamp() === 10000000 && |
||||
$n->getObjectType() === 'create' && |
||||
$n->getObjectId() === 'codes' && |
||||
$n->getSubject() === 'create_backupcodes'; |
||||
})); |
||||
|
||||
$this->invokePrivate($this->job, 'run', [['uid' => 'validUID']]); |
||||
} |
||||
} |
@ -0,0 +1,107 @@ |
||||
<?php |
||||
declare(strict_types=1); |
||||
/** |
||||
* @copyright Copyright (c) 2018, Roeland Jago Douma <roeland@famdouma.nl> |
||||
* |
||||
* @author Roeland Jago Douma <roeland@famdouma.nl> |
||||
* |
||||
* @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 OCA\TwoFactorBackupCodes\Tests\Unit\Listener; |
||||
|
||||
use OCA\TwoFactorBackupCodes\BackgroundJob\RememberBackupCodesJob; |
||||
use OCA\TwoFactorBackupCodes\Listener\ProviderEnabled; |
||||
use OCP\Authentication\TwoFactorAuth\IRegistry; |
||||
use OCP\Authentication\TwoFactorAuth\RegistryEvent; |
||||
use OCP\BackgroundJob\IJobList; |
||||
use OCP\IUser; |
||||
use Symfony\Component\EventDispatcher\Event; |
||||
use Test\TestCase; |
||||
|
||||
class ProviderEnabledTest extends TestCase { |
||||
|
||||
/** @var IRegistry|\PHPUnit\Framework\MockObject\MockObject */ |
||||
private $registy; |
||||
|
||||
/** @var IJobList|\PHPUnit\Framework\MockObject\MockObject */ |
||||
private $jobList; |
||||
|
||||
/** @var ProviderEnabled */ |
||||
private $listener; |
||||
|
||||
protected function setUp() { |
||||
parent::setUp(); |
||||
|
||||
$this->registy = $this->createMock(IRegistry::class); |
||||
$this->jobList = $this->createMock(IJobList::class); |
||||
|
||||
$this->listener = new ProviderEnabled($this->registy, $this->jobList); |
||||
} |
||||
|
||||
public function testHandleGenericEvent() { |
||||
$event = $this->createMock(Event::class); |
||||
$this->jobList->expects($this->never()) |
||||
->method($this->anything()); |
||||
|
||||
$this->listener->handle($event); |
||||
} |
||||
|
||||
public function testHandleCodesGeneratedEventAlraedyBackupcodes() { |
||||
$user = $this->createMock(IUser::class); |
||||
$user->method('getUID') |
||||
->willReturn('myUID'); |
||||
$event = $this->createMock(RegistryEvent::class); |
||||
$event->method('getUser') |
||||
->willReturn($user); |
||||
|
||||
$this->registy->method('getProviderStates') |
||||
->with($user) |
||||
->willReturn([ |
||||
'backup_codes' => true, |
||||
]); |
||||
|
||||
$this->jobList->expects($this->never()) |
||||
->method($this->anything()); |
||||
|
||||
$this->listener->handle($event); |
||||
} |
||||
|
||||
public function testHandleCodesGeneratedEventNoBackupcodes() { |
||||
$user = $this->createMock(IUser::class); |
||||
$user->method('getUID') |
||||
->willReturn('myUID'); |
||||
$event = $this->createMock(RegistryEvent::class); |
||||
$event->method('getUser') |
||||
->willReturn($user); |
||||
|
||||
$this->registy->method('getProviderStates') |
||||
->with($user) |
||||
->willReturn([ |
||||
'backup_codes' => false, |
||||
]); |
||||
|
||||
$this->jobList->expects($this->once()) |
||||
->method('add') |
||||
->with( |
||||
$this->equalTo(RememberBackupCodesJob::class), |
||||
$this->equalTo(['uid' => 'myUID']) |
||||
); |
||||
|
||||
$this->listener->handle($event); |
||||
} |
||||
} |
@ -0,0 +1,120 @@ |
||||
<?php |
||||
declare(strict_types=1); |
||||
/** |
||||
* @copyright Copyright (c) 2018, Roeland Jago Douma <roeland@famdouma.nl> |
||||
* |
||||
* @author Roeland Jago Douma <roeland@famdouma.nl> |
||||
* |
||||
* @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 OCA\TwoFactorBackupCodes\Tests\Unit\Notification; |
||||
|
||||
use OCA\TwoFactorBackupCodes\Notifications\Notifier; |
||||
use OCP\IL10N; |
||||
use OCP\L10N\IFactory; |
||||
use OCP\Notification\INotification; |
||||
use Test\TestCase; |
||||
|
||||
class NotifierTest extends TestCase { |
||||
/** @var Notifier */ |
||||
protected $notifier; |
||||
|
||||
/** @var IFactory|\PHPUnit\Framework\MockObject\MockObject */ |
||||
protected $factory; |
||||
/** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */ |
||||
protected $l; |
||||
|
||||
protected function setUp() { |
||||
parent::setUp(); |
||||
|
||||
$this->l = $this->createMock(IL10N::class); |
||||
$this->l->expects($this->any()) |
||||
->method('t') |
||||
->willReturnCallback(function($string, $args) { |
||||
return vsprintf($string, $args); |
||||
}); |
||||
$this->factory = $this->createMock(IFactory::class); |
||||
$this->factory->expects($this->any()) |
||||
->method('get') |
||||
->willReturn($this->l); |
||||
|
||||
$this->notifier = new Notifier( |
||||
$this->factory |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* @expectedException \InvalidArgumentException |
||||
*/ |
||||
public function testPrepareWrongApp() { |
||||
/** @var INotification|\PHPUnit_Framework_MockObject_MockObject $notification */ |
||||
$notification = $this->createMock(INotification::class); |
||||
$notification->expects($this->once()) |
||||
->method('getApp') |
||||
->willReturn('notifications'); |
||||
$notification->expects($this->never()) |
||||
->method('getSubject'); |
||||
|
||||
$this->notifier->prepare($notification, 'en'); |
||||
} |
||||
|
||||
/** |
||||
* @expectedException \InvalidArgumentException |
||||
*/ |
||||
public function testPrepareWrongSubject() { |
||||
/** @var INotification|\PHPUnit_Framework_MockObject_MockObject $notification */ |
||||
$notification = $this->createMock(INotification::class); |
||||
$notification->expects($this->once()) |
||||
->method('getApp') |
||||
->willReturn('twofactor_backupcodes'); |
||||
$notification->expects($this->once()) |
||||
->method('getSubject') |
||||
->willReturn('wrong subject'); |
||||
|
||||
$this->notifier->prepare($notification, 'en'); |
||||
} |
||||
|
||||
public function testPrepare() { |
||||
/** @var \OCP\Notification\INotification|\PHPUnit_Framework_MockObject_MockObject $notification */ |
||||
$notification = $this->createMock(INotification::class); |
||||
|
||||
$notification->expects($this->once()) |
||||
->method('getApp') |
||||
->willReturn('twofactor_backupcodes'); |
||||
$notification->expects($this->once()) |
||||
->method('getSubject') |
||||
->willReturn('create_backupcodes'); |
||||
|
||||
$this->factory->expects($this->once()) |
||||
->method('get') |
||||
->with('twofactor_backupcodes', 'nl') |
||||
->willReturn($this->l); |
||||
|
||||
$notification->expects($this->once()) |
||||
->method('setParsedSubject') |
||||
->with('Generate backup codes') |
||||
->willReturnSelf(); |
||||
$notification->expects($this->once()) |
||||
->method('setParsedMessage') |
||||
->with('You have enabled two-factor authentication but have not yet generated backup codes. Be sure to do this in case you lose access to your second factor.') |
||||
->willReturnSelf(); |
||||
|
||||
$return = $this->notifier->prepare($notification, 'nl'); |
||||
$this->assertEquals($notification, $return); |
||||
} |
||||
} |
Loading…
Reference in new issue