diff --git a/lib/Controller/SAMLController.php b/lib/Controller/SAMLController.php index 984cc6603..6824b7218 100644 --- a/lib/Controller/SAMLController.php +++ b/lib/Controller/SAMLController.php @@ -20,6 +20,7 @@ use OCA\User_SAML\UserResolver; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; +use OCP\EventDispatcher\IEventDispatcher; use OCP\IConfig; use OCP\IL10N; use OCP\IRequest; @@ -29,6 +30,8 @@ use OCP\Security\ICrypto; use OCP\Security\ITrustedDomainHelper; use OCP\Server; +use OCP\User\Events\UserLoggedInEvent; +use OCP\User\Events\UserLoggedOutEvent; use OneLogin\Saml2\Auth; use OneLogin\Saml2\Error; use OneLogin\Saml2\Settings; @@ -63,6 +66,7 @@ class SAMLController extends Controller { */ private $crypto; private ITrustedDomainHelper $trustedDomainHelper; + private IEventDispatcher $eventDispatcher; public function __construct( string $appName, @@ -78,7 +82,8 @@ public function __construct( UserResolver $userResolver, UserData $userData, ICrypto $crypto, - ITrustedDomainHelper $trustedDomainHelper + ITrustedDomainHelper $trustedDomainHelper, + IEventDispatcher $eventDispatcher ) { parent::__construct($appName, $request); $this->session = $session; @@ -93,6 +98,7 @@ public function __construct( $this->userData = $userData; $this->crypto = $crypto; $this->trustedDomainHelper = $trustedDomainHelper; + $this->eventDispatcher = $eventDispatcher; } /** @@ -222,12 +228,15 @@ public function login(int $idp = 1): Http\RedirectResponse { $response->addCookie('saml_data', $data, null, 'None'); break; + + case 'environment-variable': $ssoUrl = $originalUrl; if (empty($ssoUrl)) { $ssoUrl = $this->urlGenerator->getAbsoluteURL('/'); } $this->session->set('user_saml.samlUserData', $_SERVER); + try { $this->userData->setAttributes($this->session->get('user_saml.samlUserData')); $this->autoprovisionIfPossible(); @@ -235,12 +244,15 @@ public function login(int $idp = 1): Http\RedirectResponse { $firstLogin = $user->updateLastLoginTimestamp(); if ($firstLogin) { $this->userBackend->initializeHomeDir($user->getUID()); - } + } + $this->eventDispatcher->dispatchTyped(new UserLoggedInEvent($user, $user->getUID(), null, false)); + } catch (NoUserFoundException $e) { if ($e->getMessage()) { $this->logger->warning('Error while trying to login using sso environment variable: ' . $e->getMessage(), ['app' => 'user_saml']); } $ssoUrl = $this->urlGenerator->linkToRouteAbsolute('user_saml.SAML.notProvisioned'); + } catch (UserFilterViolationException $e) { $this->logger->info( 'SAML filter constraints not met: {msg}', @@ -251,11 +263,14 @@ public function login(int $idp = 1): Http\RedirectResponse { ); $ssoUrl = $this->urlGenerator->linkToRouteAbsolute('user_saml.SAML.notPermitted'); } + $response = new Http\RedirectResponse($ssoUrl); if (isset($e)) { $this->session->clear(); } break; + + default: throw new \Exception( sprintf( @@ -367,11 +382,13 @@ public function assertionConsumerService(): Http\RedirectResponse { try { $this->userData->setAttributes($auth->getAttributes()); $this->autoprovisionIfPossible(); + } catch (NoUserFoundException $e) { $this->logger->error($e->getMessage(), ['app' => $this->appName]); $response = new Http\RedirectResponse($this->urlGenerator->linkToRouteAbsolute('user_saml.SAML.notProvisioned')); $response->invalidateCookie('saml_data'); return $response; + } catch (UserFilterViolationException $e) { $this->logger->error($e->getMessage(), ['app' => $this->appName]); $response = new Http\RedirectResponse($this->urlGenerator->linkToRouteAbsolute('user_saml.SAML.notPermitted')); @@ -413,6 +430,8 @@ public function assertionConsumerService(): Http\RedirectResponse { $response->addCookie('_shibsession_', 'authenticated'); } + $this->eventDispatcher->dispatchTyped(new UserLoggedInEvent($user, $user->getUID(), null, false)); + $response->invalidateCookie('saml_data'); return $response; } @@ -425,6 +444,8 @@ public function assertionConsumerService(): Http\RedirectResponse { * @throws Error */ public function singleLogoutService(): Http\RedirectResponse { + $user = $this->userResolver->findExistingUser($this->userBackend->getCurrentUserId()); + $isFromGS = ($this->config->getSystemValue('gs.enabled', false) && $this->config->getSystemValue('gss.mode', '') === 'master'); @@ -486,10 +507,13 @@ public function singleLogoutService(): Http\RedirectResponse { } catch (Error $e) { $this->logger->warning($e->getMessage(), ['exception' => $e, 'app' => $this->appName]); $this->userSession->logout(); + $this->eventDispatcher->dispatchTyped(new UserLoggedOutEvent($user)); } } + if (!empty($targetUrl) && !$auth->getLastErrorReason()) { $this->userSession->logout(); + $this->eventDispatcher->dispatchTyped(new UserLoggedOutEvent($user)); } } if (empty($targetUrl)) { diff --git a/lib/UserBackend.php b/lib/UserBackend.php index 98dd1e02f..2a0e921a0 100644 --- a/lib/UserBackend.php +++ b/lib/UserBackend.php @@ -24,6 +24,7 @@ use OCP\User\Backend\IGetDisplayNameBackend; use OCP\User\Backend\IGetHomeBackend; use OCP\User\Events\UserChangedEvent; +use OCP\User\Events\UserDeletedEvent; use OCP\User\Events\UserFirstTimeLoggedInEvent; use OCP\UserInterface; use Psr\Log\LoggerInterface; @@ -103,41 +104,43 @@ protected function userExistsInDatabase(string $uid): bool { * @param array $attributes */ public function createUserIfNotExists(string $uid, array $attributes = []): void { - if (!$this->userExistsInDatabase($uid)) { - $values = [ - 'uid' => $uid, - ]; - - // Try to get the mapped home directory of the user - try { - $home = $this->getAttributeValue('saml-attribute-mapping-home_mapping', $attributes); - } catch (\InvalidArgumentException) { - $home = ''; - } + if ($this->userExistsInDatabase($uid)) { + return; + } - if ($home !== '') { - //if attribute's value is an absolute path take this, otherwise append it to data dir - //check for / at the beginning or pattern c:\ resp. c:/ - if ($home[0] !== '/' - && !(strlen($home) > 3 && ctype_alpha($home[0]) - && $home[1] === ':' && ($home[2] === '\\' || $home[2] === '/')) - ) { - $home = $this->config->getSystemValue('datadirectory', - \OC::$SERVERROOT.'/data') . '/' . $home; - } + $values = [ + 'uid' => $uid, + ]; - $values['home'] = $home; - } + // Try to get the mapped home directory of the user + try { + $home = $this->getAttributeValue('saml-attribute-mapping-home_mapping', $attributes); + } catch (\InvalidArgumentException) { + $home = ''; + } - $qb = $this->db->getQueryBuilder(); - $qb->insert('user_saml_users'); - foreach ($values as $column => $value) { - $qb->setValue($column, $qb->createNamedParameter($value)); + if ($home !== '') { + //if attribute's value is an absolute path take this, otherwise append it to data dir + //check for / at the beginning or pattern c:\ resp. c:/ + if ($home[0] !== '/' + && !(strlen($home) > 3 && ctype_alpha($home[0]) + && $home[1] === ':' && ($home[2] === '\\' || $home[2] === '/')) + ) { + $home = $this->config->getSystemValue('datadirectory', + \OC::$SERVERROOT.'/data') . '/' . $home; } - $qb->execute(); - $this->initializeHomeDir($uid); + $values['home'] = $home; } + + $qb = $this->db->getQueryBuilder(); + $qb->insert('user_saml_users'); + foreach ($values as $column => $value) { + $qb->setValue($column, $qb->createNamedParameter($value)); + } + $qb->execute(); + + $this->initializeHomeDir($uid); } /** @@ -169,6 +172,10 @@ public function deleteUser($uid) { $affected = $qb->delete('user_saml_users') ->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid))) ->executeStatement(); + + $user = $this->userManager->get($uid); + $this->eventDispatcher->dispatchTyped(new UserDeletedEvent($user)); + return $affected > 0; } diff --git a/tests/unit/Controller/SAMLControllerTest.php b/tests/unit/Controller/SAMLControllerTest.php index 3987ac125..773061db7 100644 --- a/tests/unit/Controller/SAMLControllerTest.php +++ b/tests/unit/Controller/SAMLControllerTest.php @@ -16,6 +16,7 @@ use OCA\User_SAML\UserResolver; use OCP\AppFramework\Http\RedirectResponse; use OCP\AppFramework\Http\TemplateResponse; +use OCP\EventDispatcher\IEventDispatcher; use OCP\IConfig; use OCP\IL10N; use OCP\IRequest; @@ -25,6 +26,7 @@ use OCP\IUserSession; use OCP\Security\ICrypto; use OCP\Security\ITrustedDomainHelper; +use OCP\User\Events\UserLoggedInEvent; use PHPUnit\Framework\MockObject\MockObject; use Psr\Log\LoggerInterface; use Test\TestCase; @@ -57,6 +59,7 @@ class SAMLControllerTest extends TestCase { /** @var SAMLController */ private $samlController; private ITrustedDomainHelper|MockObject $trustedDomainController; + private IEventDispatcher $eventDispatcher; protected function setUp(): void { parent::setUp(); @@ -74,6 +77,7 @@ protected function setUp(): void { $this->userData = $this->createMock(UserData::class); $this->crypto = $this->createMock(ICrypto::class); $this->trustedDomainController = $this->createMock(ITrustedDomainHelper::class); + $this->eventDispatcher = $this->createMock(IEventDispatcher::class); $this->l->expects($this->any())->method('t')->willReturnCallback( function ($param) { @@ -100,7 +104,8 @@ function ($param) { $this->userResolver, $this->userData, $this->crypto, - $this->trustedDomainController + $this->trustedDomainController, + $this->eventDispatcher, ); } @@ -321,6 +326,11 @@ public function testLoginWithEnvVariable(array $samlUserData, string $redirect, ->method('createUserIfNotExists') ->with('MyUid'); + $this->eventDispatcher + ->expects($this->once()) + ->method('dispatchTyped') + ->with(new UserLoggedInEvent($user, 'MyUid', null, false)); + $expected = new RedirectResponse($redirect); $result = $this->samlController->login(1); $this->assertEquals($expected, $result);