diff --git a/appinfo/info.xml b/appinfo/info.xml index e2145280e36f317ab0c527d5c487300995054460..fede48170ddee9a0978bd8a94bb4e417b194fab5 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -10,7 +10,7 @@ - 10.1.0 + 10.1.1 agpl Murena SAS EcloudAccounts diff --git a/lib/Cron/TwoFactorStateChangeJob.php b/lib/Cron/TwoFactorStateChangeJob.php new file mode 100644 index 0000000000000000000000000000000000000000..dbdd32ae31ce0717e82af3a53c4e2835b4861c2f --- /dev/null +++ b/lib/Cron/TwoFactorStateChangeJob.php @@ -0,0 +1,61 @@ +jobList = $jobList; + $this->ssoService = $ssoService; + $this->logger = $logger; + + $this->setAllowParallelRuns(true); + } + + protected function run($arguments) { + $enabled = $arguments[self::ENABLED_KEY]; + $username = $arguments[self::USERNAME_KEY]; + $tryCount = $arguments[self::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[self::TRYCOUNT_KEY] = $tryCount; + $this->jobList->scheduleAfter( + TwoFactorStateChangeJob::class, + $arguments, + self::INTERVAL_INBETWEEN_JOB_IN_SEC + ); + } + } +} diff --git a/lib/Listeners/TwoFactorStateChangedListener.php b/lib/Listeners/TwoFactorStateChangedListener.php index 323088419da3ee42974d184396f9070abf0d8e87..0ae5e8e340024d645e5f5e0d78d9613ef4e852ef 100644 --- a/lib/Listeners/TwoFactorStateChangedListener.php +++ b/lib/Listeners/TwoFactorStateChangedListener.php @@ -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); } } } diff --git a/lib/Service/SSOService.php b/lib/Service/SSOService.php index 8436d8eb440d5da2a15d51e0256e625ae3c6dd17..ee17ac45b5ed8ce8e475a08a8a59eee55d999dff 100644 --- a/lib/Service/SSOService.php +++ b/lib/Service/SSOService.php @@ -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"); @@ -64,7 +67,7 @@ class SSOService { public function migrateCredential(string $username, string $secret) : void { if($this->isNotCurrentUser($username)) { - $this->getUserId($username); + $this->setupUserId($username); } $this->deleteCredentials($username); @@ -84,7 +87,7 @@ class SSOService { public function deleteCredentials(string $username) : void { if($this->isNotCurrentUser($username)) { - $this->getUserId($username); + $this->setupUserId($username); } $credentialIds = $this->getCredentialIds(); @@ -100,7 +103,7 @@ class SSOService { public function logout(string $username) : void { if($this->isNotCurrentUser($username)) { - $this->getUserId($username); + $this->setupUserId($username); } $url = $this->ssoConfig['admin_rest_api_url'] . self::USERS_ENDPOINT . '/' . $this->currentUserId . '/logout'; @@ -109,6 +112,18 @@ class SSOService { $this->callSSOAPI($url, 'POST', [], 204); } + public function handle2FAStateChange(bool $enabled, string $username) : void { + // 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); @@ -159,7 +174,19 @@ class SSOService { return $credentialEntry; } - private function getUserId(string $username) : void { + private function setupUserId(string $username) : void { + $user = $this->userManager->get($username); + $savedOIDCUid = $this->config->getUserValue($user->getUID(), 'oidc_login', 'oidc_uid'); + + if ($savedOIDCUid !== null && trim($savedOIDCUid) !== '') { + $this->currentUserId = $savedOIDCUid; + return; + } + + $this->retrieveUserId($username); + } + + private function retrieveUserId(string $username) { $user = $this->userManager->get($username); if ($user === null) { throw new SSOAdminAPIException('Error: no user exists in cloud with username ' . $username);