Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f43390db authored by Fahim Salam Chowdhury's avatar Fahim Salam Chowdhury 👽
Browse files

feat: Implement 2FASyncRetryBGJob

Sometimes because of network issue, syncing 2FA state change with SSO
Provider failed. To mitigate this, we have implemented a background job
which will try max 4 times to resync.

issue: https://gitlab.e.foundation/e/infra/backlog/-/issues/4352
parent d46859a4
Loading
Loading
Loading
Loading
Loading
+60 −0
Original line number Diff line number Diff line
<?php

declare(strict_types=1);

namespace OCA\EcloudAccounts\Cron;

use OCA\EcloudAccounts\AppInfo\Application;
use OCA\EcloudAccounts\Service\SSOService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJobList;
use OCP\BackgroundJob\QueuedJob;
use OCP\ILogger;

// This class retry 2FA state sync with SSO provider.
// It is called from TwoFactorStateChangedListerner when it faced exception on its try.
// This class should retry max 3 times + 1 time tried on TwoFactorStateChangedListerner class.
// In this class, each retry should have interval of 60 seconds.
class TwoFactorStateChangeJob extends QueuedJob {

	public const ENABLED_KEY = 'enabled';
	public const USERNAME_KEY = 'username';
	public const TRYCOUNT_KEY = 'tryCount';

	private SSOService $ssoService;
	private IJobList $jobList;
	private ILogger $logger;

	public function __construct(ITimeFactory $time, IJobList $jobList, SSOService $ssoService, ILogger $logger) {
		parent::__construct($time);

		$this->jobList = $jobList;
		$this->ssoService = $ssoService;
		$this->logger = $logger;

		$this->setAllowParallelRuns(true);
	}

	protected function run($arguments) {
		$enabled = $arguments[$this::ENABLED_KEY];
		$username = $arguments[$this::USERNAME_KEY];
		$tryCount = $arguments[$this::TRYCOUNT_KEY];

		try {
			$this->ssoService->handle2FAStateChange($enabled, $username);
		} catch (Exception $e) {
			if ($tryCount > 2) {
				$this->logger->logException($e, ['app' => Application::APP_ID]);
				return;
			}

			$tryCount = $tryCount + 1;
			$arguments[$this::TRYCOUNT_KEY] = $tryCount;
			$this->jobList->scheduleAfter(
				TwoFactorStateChangeJob::class,
				$arguments,
				60
			);
		}
	}
}
+15 −14
Original line number Diff line number Diff line
@@ -5,27 +5,27 @@ declare(strict_types=1);
namespace OCA\EcloudAccounts\Listeners;

use OCA\EcloudAccounts\AppInfo\Application;
use OCA\EcloudAccounts\Db\TwoFactorMapper;
use OCA\EcloudAccounts\Cron\TwoFactorStateChangeJob;
use OCA\EcloudAccounts\Service\SSOService;
use OCA\TwoFactorTOTP\Event\StateChanged;
use OCP\App\IAppManager;
use OCP\BackgroundJob\IJobList;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\ILogger;

class TwoFactorStateChangedListener implements IEventListener {
	private IAppManager $appManager;
	private TwoFactorMapper $twoFactorMapper;
	private SSOService $ssoService;
	private IJobList $jobList;
	private ILogger $logger;

	private const TWOFACTOR_APP_ID = 'twofactor_totp';


	public function __construct(IAppManager $appManager, SSOService $ssoService, TwoFactorMapper $twoFactorMapper, ILogger $logger) {
	public function __construct(IAppManager $appManager, IJobList $jobList, SSOService $ssoService, ILogger $logger) {
		$this->appManager = $appManager;
		$this->ssoService = $ssoService;
		$this->twoFactorMapper = $twoFactorMapper;
		$this->jobList = $jobList;
		$this->logger = $logger;
	}

@@ -37,18 +37,19 @@ class TwoFactorStateChangedListener implements IEventListener {

		$user = $event->getUser();
		$username = $user->getUID();
		$enabled = $event->isEnabled();
		try {
			// When state change event is fired by user disabling 2FA, delete existing 2FA credentials and return
			// i.e. disable 2FA for user at SSO
			if (!$event->isEnabled()) {
				$this->ssoService->deleteCredentials($username);
				return;
			}

			$secret = $this->twoFactorMapper->getSecret($username);
			$this->ssoService->migrateCredential($username, $secret);
			$this->ssoService->handle2FAStateChange($enabled, $username);
		} catch (Exception $e) {
			$this->logger->logException($e, ['app' => Application::APP_ID]);

			// faced exception. Initiate BG job to retry again.
			$arguments = [
				TwoFactorStateChangeJob::ENABLED_KEY => $event->isEnabled(),
				TwoFactorStateChangeJob::USERNAME_KEY => $username,
				TwoFactorStateChangeJob::TRYCOUNT_KEY => 0
			];
			$this->jobList->add(TwoFactorStateChangeJob::class, $arguments);
		}
	}
}
+16 −1
Original line number Diff line number Diff line
@@ -4,6 +4,7 @@
namespace OCA\EcloudAccounts\Service;

use OCA\EcloudAccounts\AppInfo\Application;
use OCA\EcloudAccounts\Db\TwoFactorMapper;
use OCA\EcloudAccounts\Exception\SSOAdminAccessTokenException;
use OCA\EcloudAccounts\Exception\SSOAdminAPIException;
use OCP\IConfig;
@@ -25,6 +26,7 @@ class SSOService {
	private ICrypto $crypto;
	private IFactory $l10nFactory;
	private IUserManager $userManager;
	private TwoFactorMapper $twoFactorMapper;

	private string $mainDomain;
	private string $legacyDomain;
@@ -33,7 +35,7 @@ class SSOService {
	private const USERS_ENDPOINT = '/users';
	private const CREDENTIALS_ENDPOINT = '/users/{USER_ID}/credentials';

	public function __construct($appName, IConfig $config, CurlService $curlService, ICrypto $crypto, IFactory $l10nFactory, IUserManager $userManager, ILogger $logger) {
	public function __construct($appName, IConfig $config, CurlService $curlService, ICrypto $crypto, IFactory $l10nFactory, IUserManager $userManager, TwoFactorMapper $twoFactorMapper, ILogger $logger) {
		$this->appName = $appName;
		$this->config = $config;

@@ -53,6 +55,7 @@ class SSOService {
		$this->logger = $logger;
		$this->l10nFactory = $l10nFactory;
		$this->userManager = $userManager;
		$this->twoFactorMapper = $twoFactorMapper;

		$this->mainDomain = $this->config->getSystemValue("main_domain");
		$this->legacyDomain = $this->config->getSystemValue("legacy_domain");
@@ -109,6 +112,18 @@ class SSOService {
		$this->callSSOAPI($url, 'POST', [], 204);
	}

	public function handle2FAStateChange(bool $enabled, string $username) {
		// When state change event is fired by user disabling 2FA, delete existing 2FA credentials and return
		// i.e. disable 2FA for user at SSO
		if (!$enabled) {
			$this->deleteCredentials($username);
			return;
		}

		$secret = $this->twoFactorMapper->getSecret($username);
		$this->migrateCredential($username, $secret);
	}

	private function getCredentialIds() : array {
		$url = $this->ssoConfig['admin_rest_api_url'] . self::CREDENTIALS_ENDPOINT;
		$url = str_replace('{USER_ID}', $this->currentUserId, $url);