Loading lib/Cron/TwoFactorStateChangeJob.php 0 → 100644 +61 −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 const INTERVAL_INBETWEEN_JOB_IN_SEC = 60; 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[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 ); } } } lib/Listeners/TwoFactorStateChangedListener.php +15 −14 Original line number Diff line number Diff line Loading @@ -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; } Loading @@ -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); } } } lib/Service/SSOService.php +32 −5 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -25,6 +26,7 @@ class SSOService { private ICrypto $crypto; private IFactory $l10nFactory; private IUserManager $userManager; private TwoFactorMapper $twoFactorMapper; private string $mainDomain; private string $legacyDomain; Loading @@ -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; Loading @@ -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"); Loading @@ -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); Loading @@ -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(); Loading @@ -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'; Loading @@ -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); Loading Loading @@ -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); Loading Loading
lib/Cron/TwoFactorStateChangeJob.php 0 → 100644 +61 −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 const INTERVAL_INBETWEEN_JOB_IN_SEC = 60; 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[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 ); } } }
lib/Listeners/TwoFactorStateChangedListener.php +15 −14 Original line number Diff line number Diff line Loading @@ -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; } Loading @@ -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); } } }
lib/Service/SSOService.php +32 −5 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -25,6 +26,7 @@ class SSOService { private ICrypto $crypto; private IFactory $l10nFactory; private IUserManager $userManager; private TwoFactorMapper $twoFactorMapper; private string $mainDomain; private string $legacyDomain; Loading @@ -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; Loading @@ -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"); Loading @@ -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); Loading @@ -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(); Loading @@ -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'; Loading @@ -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); Loading Loading @@ -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); Loading