From 6f493c2f358bb7bfca23c9dcf8b8487dbde23793 Mon Sep 17 00:00:00 2001 From: Sonia Zorba <sonia.zorba@inaf.it> Date: Fri, 5 Mar 2021 17:14:49 +0100 Subject: [PATCH] Refactoring of login logic preparatory to autojoin --- classes/Locator.php | 4 + classes/UserHandler.php | 107 ++++++------ classes/datalayer/UserDAO.php | 4 +- classes/datalayer/mysql/MySQLUserDAO.php | 27 ++- classes/login/GmsClient.php | 71 ++++++++ classes/login/LoginHandler.php | 211 ++++++++++++++--------- classes/model/SessionData.php | 10 ++ composer.json | 3 +- css/style.css | 4 + include/front-controller.php | 36 ++-- tests/LoginFlowTest.php | 200 +++++++++++++++++++++ tests/LoginHandlerTest.php | 75 ++------ version.txt | 2 +- views/confirm-join.php | 23 ++- 14 files changed, 544 insertions(+), 233 deletions(-) create mode 100644 classes/login/GmsClient.php create mode 100644 tests/LoginFlowTest.php diff --git a/classes/Locator.php b/classes/Locator.php index e1709a2..37c2eec 100644 --- a/classes/Locator.php +++ b/classes/Locator.php @@ -98,6 +98,10 @@ class Locator { return new TokenExchanger($this); } + public function getGmsClient(): GmsClient { + return new GmsClient($this); + } + /** * Retrieve the SessionData object from the $_SESSION PHP variable. Create a * new one if it is necessary. diff --git a/classes/UserHandler.php b/classes/UserHandler.php index d7d2ba8..9d8f573 100644 --- a/classes/UserHandler.php +++ b/classes/UserHandler.php @@ -42,7 +42,7 @@ class UserHandler { * new identities to it. * @param \RAP\User $user */ - public function saveUser(User $user) { + public function saveUser(User $user): void { $primarySpecified = true; @@ -63,6 +63,25 @@ class UserHandler { } } + /** + * Update user with fresh information received by IdP. Useful for keeping email address always updated. + */ + public function updateIdentity(User $user, Identity $identity): void { + + $savedIdentity = $user->getIdentityByTypedId($identity->typedId); + $savedIdentity->email = $identity->email; + if ($identity->name !== null) { + $savedIdentity->name = $identity->name; + } + if ($identity->surname !== null) { + $savedIdentity->surname = $identity->surname; + } + if ($identity->institution !== null) { + $savedIdentity->institution = $identity->institution; + } + $this->userDAO->updateIdentity($savedIdentity); + } + public function joinUsers(User $user1, User $user2): User { $userId1 = $user1->id; @@ -72,69 +91,47 @@ class UserHandler { return $user1; } - // Call Grouper for moving groups and privileges from one user to the other - if (isset($this->locator->config->gms)) { - - //create cURL connection - $conn = curl_init($this->locator->config->gms->joinEndpoint); - - //set options - curl_setopt($conn, CURLOPT_CONNECTTIMEOUT, 30); - curl_setopt($conn, CURLOPT_RETURNTRANSFER, true); - curl_setopt($conn, CURLOPT_SSL_VERIFYPEER, true); - curl_setopt($conn, CURLOPT_FOLLOWLOCATION, 1); - // Setting an empty body, otherwise Content-Length will be negative and Spring will answer with 400 - curl_setopt($conn, CURLOPT_POSTFIELDS, " "); - curl_setopt($conn, CURLOPT_HTTPHEADER, ['Authorization: Bearer ' - . $this->getJoinAccessToken($userId1, $userId2)]); - - //set data to be posted - curl_setopt($conn, CURLOPT_POST, 1); - - //perform the request - $response = curl_exec($conn); - $info = curl_getinfo($conn); - - if ($info['http_code'] === 200) { - curl_close($conn); - } else { - //show information regarding the error - curl_close($conn); - error_log($response); - $httpCode = $info['http_code']; - if ($httpCode === 0) { - throw new ServerErrorException('GMS service is unreachable'); - } - throw new ServerErrorException('Error: GMS response code: ' . $httpCode); - } + if ($userId1 === null && $userId2 === null) { + throw new BadRequestException("Called join on two users that are both new"); + } + + if ($userId1 === null) { + return $this->joinNewIdentity($user2, $user1); } + if ($userId2 === null) { + return $this->joinNewIdentity($user1, $user2); + } + + // Call Grouper for moving groups and privileges from one user to the other + $remainingUserId = $this->locator->getGmsClient()->joinGroups($userId1, $userId2); + + $remainingUser = $userId1 === $remainingUserId ? $user1 : $user2; + $userToDelete = $userId1 === $remainingUserId ? $user2 : $user1; // Call DAO for performing join operation into the RAP database. - $this->userDAO->joinUsers($userId1, $userId2); + $this->userDAO->joinUsers($remainingUserId, $userToDelete->id); - foreach ($user2->identities as $identity) { - $identity->primary = false; - $user1->addIdentity($identity); - } + $this->moveIdentities($remainingUser, $userToDelete); // merged user - return $user1; + return $remainingUser; } - private function getJoinAccessToken(int $userId1, int $userId2): string { - - $gmsId = $this->locator->config->gms->id; - - $accessToken = new AccessTokenData(); - $accessToken->clientId = $gmsId; - $accessToken->userId = $userId1; - // shorter expiration - $accessToken->expirationTime = $accessToken->creationTime + 100; - $accessToken->scope = ['openid']; + /** + * New identity, not yet associated with a user: simply add it to the other user. + */ + private function joinNewIdentity(User $user, User $newUser): User { + $identity = $newUser->identities[0]; + $user->addIdentity($identity); + $this->saveUser($user); + return $user; + } - return $this->locator->getTokenBuilder()->getAccessToken($accessToken, function(& $jwt) use($userId2) { - $jwt['alt_sub'] = strval($userId2); - }); + private function moveIdentities(User $remainingUser, User $deletedUser) { + foreach ($deletedUser->identities as $identity) { + $identity->primary = false; + $remainingUser->addIdentity($identity); + } } } diff --git a/classes/datalayer/UserDAO.php b/classes/datalayer/UserDAO.php index ea9be27..436f755 100644 --- a/classes/datalayer/UserDAO.php +++ b/classes/datalayer/UserDAO.php @@ -80,5 +80,7 @@ interface UserDAO { function updateIdentity(Identity $identity): void; - function findJoinableUsers($userId): array; + function findJoinableUsersByEmail(string $email): array; + + function findJoinableUsersByUserId(string $userId): array; } diff --git a/classes/datalayer/mysql/MySQLUserDAO.php b/classes/datalayer/mysql/MySQLUserDAO.php index a188f27..d65bc7b 100644 --- a/classes/datalayer/mysql/MySQLUserDAO.php +++ b/classes/datalayer/mysql/MySQLUserDAO.php @@ -301,10 +301,33 @@ class MySQLUserDAO extends BaseMySQLDAO implements UserDAO { $stmt->execute(); } - function findJoinableUsers($userId): array { - + function findJoinableUsersByEmail(string $email): array { + $dbh = $this->getDBHandler(); + + $query = "SELECT DISTINCT(i3.user_id)\n" + . "FROM user u\n" + . "JOIN identity i ON i.user_id = u.id\n" + . "JOIN identity i2 ON i.user_id = i2.user_id\n" + . "JOIN identity i3 ON i2.email = i3.email\n" + . "WHERE i.email = :email"; + + $stmt = $dbh->prepare($query); + $stmt->bindParam(':email', $email); + + $stmt->execute(); + $results = []; + foreach ($stmt->fetchAll() as $row) { + array_push($results, $row['user_id']); + } + return $results; + } + + function findJoinableUsersByUserId(string $userId): array { + + $dbh = $this->getDBHandler(); + $query = "SELECT DISTINCT(i3.user_id)\n" . "FROM user u\n" . "JOIN identity i ON i.user_id = u.id\n" diff --git a/classes/login/GmsClient.php b/classes/login/GmsClient.php new file mode 100644 index 0000000..7efacb1 --- /dev/null +++ b/classes/login/GmsClient.php @@ -0,0 +1,71 @@ +<?php + +namespace RAP; + +class GmsClient { + + private $locator; + + public function __construct(Locator $locator) { + $this->locator = $locator; + } + + public function joinGroups(string $userId1, string $userId2): string { + if (!isset($this->locator->config->gms)) { + return $userId1; + } + + //create cURL connection + $conn = curl_init($this->locator->config->gms->joinEndpoint); + + //set options + curl_setopt($conn, CURLOPT_CONNECTTIMEOUT, 30); + curl_setopt($conn, CURLOPT_RETURNTRANSFER, true); + curl_setopt($conn, CURLOPT_SSL_VERIFYPEER, true); + curl_setopt($conn, CURLOPT_FOLLOWLOCATION, 1); + // Setting an empty body, otherwise Content-Length will be negative and Spring will answer with 400 + curl_setopt($conn, CURLOPT_POSTFIELDS, " "); + curl_setopt($conn, CURLOPT_HTTPHEADER, ['Authorization: Bearer ' + . $this->getJoinAccessToken($userId1, $userId2)]); + + //set data to be posted + curl_setopt($conn, CURLOPT_POST, 1); + + //perform the request + $response = curl_exec($conn); + $info = curl_getinfo($conn); + + if ($info['http_code'] === 200) { + curl_close($conn); + } else { + //show information regarding the error + curl_close($conn); + error_log($response); + $httpCode = $info['http_code']; + if ($httpCode === 0) { + throw new ServerErrorException('GMS service is unreachable'); + } + throw new ServerErrorException('Error: GMS response code: ' . $httpCode); + } + + // TODO: return id extracted from GMS response + return $userId1; + } + + private function getJoinAccessToken(int $userId1, int $userId2): string { + + $gmsId = $this->locator->config->gms->id; + + $accessToken = new AccessTokenData(); + $accessToken->clientId = $gmsId; + $accessToken->userId = $userId1; + // shorter expiration + $accessToken->expirationTime = $accessToken->creationTime + 100; + $accessToken->scope = ['openid']; + + return $this->locator->getTokenBuilder()->getAccessToken($accessToken, function(& $jwt) use($userId2) { + $jwt['alt_sub'] = strval($userId2); + }); + } + +} diff --git a/classes/login/LoginHandler.php b/classes/login/LoginHandler.php index 79148b8..571421e 100644 --- a/classes/login/LoginHandler.php +++ b/classes/login/LoginHandler.php @@ -2,6 +2,9 @@ namespace RAP; +/** + * All public methods return a redirect URL. + */ class LoginHandler { protected $locator; @@ -10,41 +13,116 @@ class LoginHandler { $this->locator = $locator; } - /** - * Returns redirect URL. - */ public function onIdentityDataReceived(Identity $identity): string { - - $this->locator->getSession()->setLoginIdentityType($identity->type); + + $session = $this->locator->getSession(); + $session->setLoginIdentityType($identity->type); + + $user = $this->getUser($identity); + + $joinStep = $this->checkJoinAction($session, $user); + if ($joinStep !== null) { + return $joinStep; + } + + $session->setUser($user); + + $autoJoinStep = $this->checkAutoJoin(); + if ($autoJoinStep !== null) { + return $autoJoinStep; + } + + if ($user->id === null) { + return $this->redirectToTOUCheck($user); + } else { + $this->locator->getUserHandler()->updateIdentity($user, $identity); + } + + return $this->getAfterLoginRedirect(); + } + + private function getUser(Identity $identity): User { $userDao = $this->locator->getUserDAO(); - + $user = $userDao->findUserByIdentity($identity->type, $identity->typedId); - + if ($user === null) { - return $this->handleNewIdentity($identity); - } else { - $this->updateUser($user, $identity); + // create new user with null id + $user = new User(); + $user->addIdentity($identity); } + return $user; + } + + private function checkJoinAction(SessionData $session, User $user): ?string { + if ($session->getOAuth2RequestData() === null && $session->getAction() === 'join' && $session->getUser() !== null) { + if ($session->getUser()->id === $user->id) { + // attempting to join an already joined user: go to account page without joining + $session->setAction('account'); + } else { + return $this->showConfirmJoin($user); + } + } + return null; + } + + public function confirmJoin(): string { + $session = $this->locator->getSession(); - if ($session->getOAuth2RequestData() === null && $session->getAction() === 'join' && - $session->getUser() !== null && $session->getUser()->id !== $user->id) { - return $this->showConfirmJoin($user); + + if ($session->getUserToJoin() === null) { + throw new BadRequestException("Unable to find user to join"); } - return $this->getAfterLoginRedirect($user); + $this->joinUsers(); + + $autoJoinStep = $this->checkAutoJoin(); + if ($autoJoinStep !== null) { + return $autoJoinStep; + } + + return $this->getAfterLoginRedirect(); } - protected function handleNewIdentity(Identity $identity): string { + private function checkAutoJoin(): ?string { + $userDao = $this->locator->getUserDAO(); $session = $this->locator->getSession(); + $user = $session->getUser(); - if ($session->getUser() !== null && $session->getAction() === 'join') { - $userToJoin = $this->getNewUser($identity); + if ($user->id === null) { + $joinableUsers = $userDao->findJoinableUsersByEmail($user->identities[0]->email); + } else { + $joinableUsers = $userDao->findJoinableUsersByUserId($user->id); + } + + if (count($joinableUsers) > 0) { + // select first user + $userToJoin = $userDao->findUserById($joinableUsers[0]); + $session->setAutojoin(true); return $this->showConfirmJoin($userToJoin); + } + + return null; + } + + public function rejectJoin(): string { + + $session = $this->locator->getSession(); + + $user = $session->getUser(); + if ($user === null) { + throw new \RAP\BadRequestException("Unable to find user"); + } + + $session->setJoinRejected(true); + + if ($session->getUser()->id === null) { + return $this->redirectToTOUCheck($session->getUser()->identities[0]); } else { - return $this->redirectToTOUCheck($identity); + return $this->getAfterLoginRedirect(); } } @@ -56,94 +134,63 @@ class LoginHandler { /** * Stores the data into session and Redirect to Term of Use acceptance page. */ - private function redirectToTOUCheck(Identity $identity): string { - - // Create new user - $user = $this->getNewUser($identity); - - $this->locator->getSession()->setUser($user); - + private function redirectToTOUCheck(): string { return $this->locator->getBasePath() . '/tou-check'; } - private function getNewUser(Identity $identity): User { - $user = new User(); - $user->addIdentity($identity); - return $user; - } - /** - * Update user with fresh information received by IdP. Useful for keeping email address always updated. + * Stores the user data into the database after he/she accepted the Terms of Use. */ - private function updateUser(User $user, Identity $identity): void { - $savedIdentity = $user->getIdentityByTypedId($identity->typedId); - $savedIdentity->email = $identity->email; - if($identity->name !== null) { - $savedIdentity->name = $identity->name; - } - if($identity->surname !== null) { - $savedIdentity->surname = $identity->surname; - } - if($identity->institution !== null) { - $savedIdentity->institution = $identity->institution; + public function register(): string { + $user = $this->locator->getSession()->getUser(); + + if ($user === null) { + throw new BadRequestException("User data not retrieved."); + } else { + $this->locator->getUserHandler()->saveUser($user); + return $this->getAfterLoginRedirect(); } - $this->locator->getUserDAO()->updateIdentity($savedIdentity); } - public function getAfterLoginRedirect(User $user): string { + private function joinUsers(): void { $session = $this->locator->getSession(); - $this->locator->getAuditLogger()->info("LOGIN," . $session->getLoginIdentityType() . "," . $user->id); + $user = $session->getUser(); + $userToJoin = $session->getUserToJoin(); - if ($session->getOAuth2RequestData() !== null) { - $session->setUser($user); - $redirectUrl = $this->locator->getOAuth2RequestHandler()->getRedirectResponseUrl(); - session_destroy(); - return $redirectUrl; + if ($user === null) { + $session->setUser($userToJoin); + } else { + $joinedUser = $this->locator->getUserHandler()->joinUsers($userToJoin, $user); + $session->setUser($joinedUser); } - if ($session->getAction() !== null) { - - $action = $session->getAction(); - - if ($action === 'join') { - $user = $this->joinTo($user); - $action = 'account'; - $session->setAction($action); - } - - $session->setUser($user); - - if ($action === 'account' || $action === 'admin') { - return $this->locator->getBasePath() . '/' . $action; - } + if ($session->getAction() === 'join') { + $session->setAction('account'); } - throw new \Exception("Unable to find a proper redirect"); + $session->setUserToJoin(null); } - private function joinTo(User $userToJoin): User { + private function getAfterLoginRedirect(): string { $session = $this->locator->getSession(); - $user = $session->getUser(); + $this->locator->getAuditLogger()->info("LOGIN," . $session->getLoginIdentityType() . "," . $session->getUser()->id); - if ($user === null) { - return $userToJoin; + if ($session->getOAuth2RequestData() !== null) { + // Redirect to OAuth2 client callback URL + $redirectUrl = $this->locator->getOAuth2RequestHandler()->getRedirectResponseUrl(); + session_destroy(); + return $redirectUrl; } - if ($userToJoin->id === null) { - // New identity, not yet associated with an user: simply add it to - // previously logged in user. - $identity = $userToJoin->identities[0]; - $user->addIdentity($identity); - $this->locator->getUserHandler()->saveUser($user); - } else if ($user->id !== $userToJoin->id) { - $user = $this->locator->getUserHandler()->joinUsers($user, $userToJoin); - } + $action = $session->getAction(); - $session->setUserToJoin(null); + if ($action === 'account' || $action === 'admin') { + return $this->locator->getBasePath() . '/' . $action; + } - return $user; + throw new \Exception("Unable to find a proper redirect"); } } diff --git a/classes/model/SessionData.php b/classes/model/SessionData.php index e1916dc..7d4d2d4 100644 --- a/classes/model/SessionData.php +++ b/classes/model/SessionData.php @@ -39,6 +39,7 @@ class SessionData { private $action; private $loginIdentityType; private $autojoin = false; + private $joinRejected = false; public function setUser(?User $user): void { $this->user = $user; @@ -67,6 +68,15 @@ class SessionData { return $this->autojoin; } + public function setJoinRejected(bool $joinRejected): void { + $this->joinRejected = $joinRejected; + $this->save(); + } + + public function isJoinRejected(): bool { + return $this->joinRejected; + } + /** * Used for logging the identity type chosen for the login at the end of the login process */ diff --git a/composer.json b/composer.json index 41daa1a..8701629 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,8 @@ "phpmailer/phpmailer": "^6.0" }, "require-dev": { - "phpunit/phpunit": "^8.2" + "phpunit/phpunit": "^8.2", + "phpmd/phpmd": "@stable" }, "autoload": { "classmap": [ diff --git a/css/style.css b/css/style.css index 124f933..a1d2319 100644 --- a/css/style.css +++ b/css/style.css @@ -221,3 +221,7 @@ body { left: 27px; top: 0px; } + +.inline { + display: inline; +} diff --git a/include/front-controller.php b/include/front-controller.php index 3ef1c86..71d85d9 100644 --- a/include/front-controller.php +++ b/include/front-controller.php @@ -242,7 +242,7 @@ Flight::route('GET /x509-name-surname', function() { } else { // Redirect to index header("Location: " . $locator->getBasePath()); - die(); + throw new \RAP\BadRequestException(); } }); @@ -265,7 +265,7 @@ Flight::route('POST /submit-x509-name', function() { $redirect = $loginHandler->afterNameSurnameSelection($session->getX509DataToRegister()); Flight::redirect($redirect); } else { - die('X.509 data not returned'); + throw new \RAP\BadRequestException('X.509 data not returned'); } }); @@ -278,7 +278,7 @@ Flight::route('GET /tou-check', function() { global $locator; if ($locator->getSession()->getUser() === null) { - die("User data not retrieved."); + throw new \RAP\BadRequestException("User data not retrieved."); } else { Flight::render('tou-check.php', array('title' => 'Terms of Use acceptance', 'user' => $locator->getSession()->getUser(), @@ -294,7 +294,7 @@ Flight::route('GET /confirm-join', function() { global $locator; if ($locator->getSession()->getUser() === null) { - die("User data not retrieved."); + throw new \RAP\BadRequestException("User data not retrieved."); } else { Flight::render('confirm-join.php', array('title' => 'Confirm join', 'autojoin' => $locator->getSession()->isAutojoin(), @@ -309,14 +309,16 @@ Flight::route('POST /confirm-join', function() { session_start(); global $locator; + $loginHandler = new \RAP\LoginHandler($locator); + Flight::redirect($loginHandler->confirmJoin()); +}); - $user = $locator->getSession()->getUserToJoin(); - if ($user === null) { - die("Unable to find user to join"); - } else { - $loginHandler = new \RAP\LoginHandler($locator); - Flight::redirect($loginHandler->getAfterLoginRedirect($user)); - } +Flight::route('POST /reject-join', function() { + + session_start(); + global $locator; + $loginHandler = new \RAP\LoginHandler($locator); + Flight::redirect($loginHandler->rejectJoin()); }); /** @@ -327,16 +329,8 @@ Flight::route('GET /register', function() { session_start(); global $locator; - $user = $locator->getSession()->getUser(); - - if ($user === null) { - die("User data not retrieved."); - } else { - $locator->getUserHandler()->saveUser($user); - - $loginHandler = new \RAP\LoginHandler($locator); - Flight::redirect($loginHandler->getAfterLoginRedirect($user)); - } + $loginHandler = new \RAP\LoginHandler($locator); + $loginHandler->register(); }); /** diff --git a/tests/LoginFlowTest.php b/tests/LoginFlowTest.php new file mode 100644 index 0000000..80b3086 --- /dev/null +++ b/tests/LoginFlowTest.php @@ -0,0 +1,200 @@ +<?php + +use PHPUnit\Framework\TestCase; + +if (session_status() === PHP_SESSION_NONE) { + session_start(); +} + +/** + * This test has a setup similar to the LoginHandlerTest one, however SessionData and + * UserHandler are not mocked, so interaction between session and login handler can be + * tested and complete login and join flows can be simulated step by step. + */ +final class LoginFlowTest extends TestCase { + + private $locatorStub; + private $userDaoStub; + private $oAuth2RequestHandler; + private $auditLogger; + private $gmsClientStub; + private $sessionData; + private $userHandler; + private $loginHandler; + + public function setUp(): void { + $this->locatorStub = $this->createMock(\RAP\Locator::class); + $this->locatorStub->method('getBasePath')->willReturn('http://rap-ia2'); + + $this->userDaoStub = $this->createMock(\RAP\UserDAO::class); + $this->locatorStub->method('getUserDAO')->willReturn($this->userDaoStub); + + $this->oAuth2RequestHandler = $this->createMock(\RAP\OAuth2RequestHandler::class); + $this->locatorStub->method('getOAuth2RequestHandler')->willReturn($this->oAuth2RequestHandler); + + $this->auditLogger = $this->createMock(\Monolog\Logger::class); + $this->locatorStub->method('getAuditLogger')->willReturn($this->auditLogger); + + $this->gmsClientStub = $this->createMock(\RAP\GmsClient::class); + $this->locatorStub->method('getGmsClient')->willReturn($this->gmsClientStub); + + $this->sessionData = new \RAP\SessionData(); + $this->locatorStub->method('getSession')->willReturn($this->sessionData); + + $this->userHandler = new \RAP\UserHandler($this->locatorStub); + $this->locatorStub->method('getUserHandler')->willReturn($this->userHandler); + + $this->loginHandler = new \RAP\LoginHandler($this->locatorStub); + } + + public function testNewIdentityLogin(): void { + + $this->sessionData->setAction('account'); + + $identity = $this->getFakeIdentity1(); + + // verify DAO is called for checking user existence + $this->userDaoStub->expects($this->once()) + ->method('findUserByIdentity')->with($identity->type, $identity->typedId); + + $redirect1 = $this->loginHandler->onIdentityDataReceived($identity); + + // verify ids are not set + $this->assertNull($this->sessionData->getUser()->id); + $this->assertNull($this->sessionData->getUser()->identities[0]->id); + + $this->assertEquals("http://rap-ia2/tou-check", $redirect1); + + $this->userDaoStub->method('createUser')->willReturn('2'); + $this->userDaoStub->method('insertIdentity')->willReturn('3'); + + $redirect2 = $this->loginHandler->register(); + + $this->assertEquals('2', $this->sessionData->getUser()->id); + $this->assertEquals('3', $this->sessionData->getUser()->identities[0]->id); + + $this->assertEquals("http://rap-ia2/account", $redirect2); + } + + public function testExistingUserLogin(): void { + + $this->sessionData->setAction('account'); + + $user = $this->getFakeUser1(); + $this->assertEquals('test@example.com', $user->identities[0]->email); + + $this->userDaoStub->method('findUserByIdentity')->willReturn($user); + + $this->userDaoStub->expects($this->once()) + ->method('updateIdentity')->with($this->anything()); + + $redirect = $this->loginHandler->onIdentityDataReceived($this->getFakeIdentity1NewEmail()); + + // verify email update + $this->assertEquals('test2@example.com', $user->identities[0]->email); + + $this->assertEquals('http://rap-ia2/account', $redirect); + } + + public function testNewIdentityAutojoin(): void { + + $this->oAuth2RequestHandler->method('getRedirectResponseUrl')->willReturn('http://redirect-url'); + + $this->userDaoStub->method('findJoinableUsersByEmail')->willReturn(['1', '2']); + $this->userDaoStub->method('findUserById')->willReturn($this->getFakeUser1()); + + $redirect1 = $this->loginHandler->onIdentityDataReceived($this->getFakeIdentity3()); + + $this->assertTrue($this->sessionData->isAutojoin()); + $this->assertEquals('http://rap-ia2/confirm-join', $redirect1); + $this->assertNotNull($this->sessionData->getUserToJoin()); + $this->assertNull($this->sessionData->getUser()->id); + + $this->userDaoStub->method('findJoinableUsersByUserId')->willReturn(['2']); + + $redirect2 = $this->loginHandler->confirmJoin(); + + $this->assertEquals('1', $this->sessionData->getUser()->id); + $this->assertEquals('http://rap-ia2/confirm-join', $redirect2); + + $this->gmsClientStub->method('joinGroups')->willReturn('2'); + + $this->gmsClientStub->expects($this->once()) + ->method('joinGroups')->with($this->anything()); + + $redirect3 = $this->loginHandler->confirmJoin(); + + $this->assertEquals('2', $this->sessionData->getUser()->id); + $this->assertEquals('http://redirect-url', $redirect3); + } + + public function testJoinAlreadyJoinedUser(): void { // go to account page without joining + $user = $this->getFakeUser1(); + + $this->sessionData->setAction('join'); + $this->sessionData->setUser($user); + + $this->userDaoStub->method('findUserByIdentity')->willReturn($user); + + $redirect = $this->loginHandler->onIdentityDataReceived($this->getFakeIdentity1()); + + $this->assertEquals('http://rap-ia2/account', $redirect); + } + + private function getFakeUser1(): \RAP\User { + + $user = new \RAP\User(); + $user->id = '1'; + $identity = new \RAP\Identity('eduGAIN'); + $identity->email = 'test@example.com'; + $identity->id = '4'; + $identity->typedId = '123'; + $user->addIdentity($identity); + return $user; + } + + private function getFakeUser2(): \RAP\User { + + $user = new \RAP\User(); + $user->id = '2'; + $identity = new \RAP\Identity('eduGAIN'); + $identity->email = 'test@example.com'; + $identity->id = '6'; + $identity->typedId = '999'; + $user->addIdentity($identity); + return $user; + } + + private function getFakeIdentity1(): \RAP\Identity { + + $identity = new \RAP\Identity(\RAP\Identity::EDU_GAIN); + $identity->typedId = '123'; + $identity->email = 'test@example.com'; + return $identity; + } + + private function getFakeIdentity1NewEmail(): \RAP\Identity { + + $identity = new \RAP\Identity(\RAP\Identity::EDU_GAIN); + $identity->typedId = '123'; + $identity->email = 'test2@example.com'; + return $identity; + } + + private function getFakeIdentity2(): \RAP\Identity { + + $identity = new \RAP\Identity(\RAP\Identity::EDU_GAIN); + $identity->typedId = '456'; + $identity->email = 'test3@example.com'; + return $identity; + } + + private function getFakeIdentity3(): \RAP\Identity { + + $identity = new \RAP\Identity(\RAP\Identity::GOOGLE); + $identity->typedId = '789'; + $identity->email = 'test@example.com'; + return $identity; + } + +} diff --git a/tests/LoginHandlerTest.php b/tests/LoginHandlerTest.php index e8cf6f1..3540aa4 100644 --- a/tests/LoginHandlerTest.php +++ b/tests/LoginHandlerTest.php @@ -2,13 +2,16 @@ use PHPUnit\Framework\TestCase; -session_start(); +if (session_status() === PHP_SESSION_NONE) { + session_start(); +} final class LoginHandlerTest extends TestCase { private $locatorStub; private $userDaoStub; private $sessionStub; + private $userHandlerStub; private $oAuth2RequestHandler; private $auditLogger; private $loginHandler; @@ -23,39 +26,30 @@ final class LoginHandlerTest extends TestCase { $this->sessionStub = $this->createMock(\RAP\SessionData::class); $this->locatorStub->method('getSession')->willReturn($this->sessionStub); + $this->userHandlerStub = $this->createMock(\RAP\UserHandler::class); + $this->locatorStub->method('getUserHandler')->willReturn($this->userHandlerStub); + $this->oAuth2RequestHandler = $this->createMock(\RAP\OAuth2RequestHandler::class); $this->locatorStub->method('getOAuth2RequestHandler')->willReturn($this->oAuth2RequestHandler); $this->auditLogger = $this->createMock(\Monolog\Logger::class); $this->locatorStub->method('getAuditLogger')->willReturn($this->auditLogger); - $this->loginHandler = new \RAP\LoginHandler($this->locatorStub, 'eduGAIN'); - } - - public function testTOUCheck(): void { - $redirect = $this->loginHandler->onIdentityDataReceived($this->getFakeIdentity1()); - - $this->assertEquals('http://rap-ia2/tou-check', $redirect); + $this->loginHandler = new \RAP\LoginHandler($this->locatorStub); } public function testExistingUserLogin(): void { $user = $this->getFakeUser(); - $this->assertEquals('test@example.com', $user->identities[0]->email); $this->userDaoStub->method('findUserByIdentity')->willReturn($user); $this->oAuth2RequestHandler->method('getRedirectResponseUrl')->willReturn('http://redirect-url'); $this->sessionStub->method('getOAuth2RequestData')->willReturn(new \RAP\OAuth2RequestData()); + $this->sessionStub->method('getUser')->willReturn($user); - $this->userDaoStub->expects($this->once()) - ->method('updateIdentity')->with($this->anything()); - - $redirect = $this->loginHandler->onIdentityDataReceived($this->getFakeIdentity1NewEmail()); - - // verify update - $this->assertEquals('test2@example.com', $user->identities[0]->email); + $redirect = $this->loginHandler->onIdentityDataReceived($this->getFakeIdentity1()); $this->assertEquals('http://redirect-url', $redirect); } @@ -91,47 +85,6 @@ final class LoginHandlerTest extends TestCase { $this->assertEquals('http://rap-ia2/confirm-join', $redirect); } - public function testJoinExistingUser(): void { - - $user = $this->getFakeUser(); - $this->sessionStub->method('getUser')->willReturn($user); - $this->sessionStub->method('getAction')->willReturn('join'); - - $userToJoin = $this->getFakeUser(); - $userToJoin->id = '456'; - - $redirect = $this->loginHandler->getAfterLoginRedirect($userToJoin); - - $this->assertEquals('http://rap-ia2/account', $redirect); - } - - public function testJoinNewIdentity(): void { - - $user = $this->getFakeUser(); - $this->sessionStub->method('getUser')->willReturn($user); - $this->sessionStub->method('getAction')->willReturn('join'); - - $userToJoin = $this->getFakeUser(); - // new identity - $userToJoin->id = null; - - $redirect = $this->loginHandler->getAfterLoginRedirect($userToJoin); - - $this->assertEquals('http://rap-ia2/account', $redirect); - } - - public function testJoinAlreadyJoinedUser(): void { // go to account page without joining - $user = $this->getFakeUser(); - - $this->sessionStub->method('getAction')->willReturn('join'); - - $this->userDaoStub->method('findUserByIdentity')->willReturn($user); - - $redirect = $this->loginHandler->onIdentityDataReceived($this->getFakeIdentity1()); - - $this->assertEquals('http://rap-ia2/account', $redirect); - } - private function getFakeUser(): \RAP\User { $user = new \RAP\User(); @@ -152,14 +105,6 @@ final class LoginHandlerTest extends TestCase { return $identity; } - private function getFakeIdentity1NewEmail(): \RAP\Identity { - - $identity = new \RAP\Identity(\RAP\Identity::EDU_GAIN); - $identity->typedId = '123'; - $identity->email = 'test2@example.com'; - return $identity; - } - private function getFakeIdentity2(): \RAP\Identity { $identity = new \RAP\Identity(\RAP\Identity::EDU_GAIN); diff --git a/version.txt b/version.txt index e9307ca..50ffc5a 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2.0.2 +2.0.3 diff --git a/views/confirm-join.php b/views/confirm-join.php index 6a4e12f..efab33d 100644 --- a/views/confirm-join.php +++ b/views/confirm-join.php @@ -6,9 +6,16 @@ include 'include/header.php'; <div class="text-center"> <?php if ($autojoin) { ?> <h3>Multiple accounts detected</h3><br/> - <p>The system found multiple accounts associated with the same e-mail address. We suggest you to join them, so that - they will be seen as a single user. If you prefer to keep these accounts separated you can click on "Reject join" button. - </p> + <div class="alert alert-info"> + <p> + <span class="glyphicon glyphicon-info-sign"></span> + The system found multiple accounts associated with the same e-mail address. We <strong>suggest</strong> you to join them, so that + they will be seen as a single user.<br/><br/> + If you prefer to keep these accounts separated you can click on "Reject join" button. + If you reject the join we will not show this message again for the same account pair, however you can still join the accounts manually + (see the <a href="https://sso.ia2.inaf.it/home/index.php?lang=en" target="blank_">Help Page</a> for more information). + </p> + </div> <?php } else { ?> <h3>Following identities will be joined:</h3><br/> <?php } ?> @@ -45,10 +52,16 @@ include 'include/header.php'; <div class="text-center"> <br/> - <form action="confirm-join" method="POST"> + <form action="confirm-join" method="POST" class="inline"> <input type="submit" value="Confirm join" class="btn btn-success btn-lg" /> </form> - <br/><br/><br/> + <?php if ($autojoin) { ?> + + <form action="reject-join" method="POST" class="inline"> + <input type="submit" value="Reject join" class="btn btn-danger btn-lg" /> + </form></form> +<?php } ?> +<br/><br/><br/> </div> <?php -- GitLab