diff --git a/README.md b/README.md index 99cf896e6bcc09502d53100f30415da85bded2fc..4e425b9dfc2834a7e48968661e929c654a55ff26 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,16 @@ -# RAP 2 +# RAP IA2 -## Installation +## Installation and configuration + +Requirements: + +* Apache httpd server (tested on Apache/2.4.6) +* PHP (5.4+), composer for dependecies +* MySQL/MariaDB (tested on MariaDB 5.5.52) + +### PHP + +Put RAP sources in `/var/www/html/rap-ia2` For installing PHP dependencies run: @@ -8,7 +18,82 @@ For installing PHP dependencies run: Install also the bcmath PHP package (used in X.509 parser). -To setup the database edit scripts in the sql folder and run them: +### MySQL + +Create a dedicated database and user: + + CREATE DATABASE rap; + CREATE USER rap@localhost IDENTIFIED BY 'XXXXXX'; + GRANT ALL PRIVILEGES ON rap.* TO rap@localhost; + +Enable the event scheduler: + +* open MySQL configuration file (e.g. /etc/my.cnf) +* set `event_scheduler=1` +* restart MySQL + +Then run the setup script: + + mysql -u root -p < sql/setup-database.sql + +### Apache (httpd) + +* Configure a valid HTTPS certificate on the server +* Configure X.509 client certificate authentication: + + <Directory /var/www/html/rap-ia2/auth/x509/> + Options Indexes FollowSymLinks + AllowOverride None + Order allow,deny + allow from all + SSLVerifyClient require + SSLVerifyDepth 10 + SSLOptions +ExportCertData + </Directory> + +* Shibboleth authentication: + + <Directory /var/www/html/rap-ia2/auth/saml2/> + AuthType shibboleth + ShibRequestSetting requireSession 1 + Require valid-user + </Directory> + +* Protect log directory: + + <Directory /var/www/html/rap-ia2/logs/> + Order deny,allow + Deny From All + </Directory> + +* Protect RAP Web Service in Basic-Auth: + + <Location "/rap-ia2/ws"> + AuthType basic + AuthName RAP + AuthUserFile apachepasswd + Require valid-user + </Location> + +* Then creates a password file for RAP Web Service Basic-Auth: + * `cd /etc/httpd/` + * `htpasswd -c apachepasswd rap` + * The last command creates an hashed password for an user "rap" and store it in a file named apachepasswd. + +* Finally, restart the Apache server. + +### Social networks + +Before using social API it is necessary to register an application on each social network and obtain API keys and secrets: + +* https://console.developers.google.com +* https://www.linkedin.com/developer/apps +* https://developers.facebook.com/apps + +### Configuration file + +Copy the `config-example.php` into `config.php` and edit it for matching your needs. + +## Additional information and developer guide - mysql -u root -p < sql/create-db-and-user.sql - mysql -u root -p rap < sql/create-tables.sql +See the wiki: https://www.ict.inaf.it/gitlab/zorba/rap-ia2/wikis/home diff --git a/auth/oauth2/facebook_login.php b/auth/oauth2/facebook_login.php index e6d59870a30602330fb5c82222bf88f889163354..844cf9f6a6dc8c677f1f43c1229a89e949d3bda3 100755 --- a/auth/oauth2/facebook_login.php +++ b/auth/oauth2/facebook_login.php @@ -22,9 +22,12 @@ * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +/* This page uses the Facebook API for generating the redirect URL to use for Facebook login */ + include '../../include/init.php'; startSession(); +// Retrieve Facebook configuration $Facebook = $AUTHENTICATION_METHODS['Facebook']; $fb = new Facebook\Facebook([ @@ -35,7 +38,8 @@ $fb = new Facebook\Facebook([ $helper = $fb->getRedirectLoginHelper(); -$permissions = ['email']; // Optional permissions +$permissions = ['email']; // Optional permissions: we need user email + $loginUrl = $helper->getLoginUrl($Facebook['callback'], $permissions); header("Location: $loginUrl"); diff --git a/auth/oauth2/facebook_token.php b/auth/oauth2/facebook_token.php index bd431656877232218597121b121086f3c0d85f09..d4383144134ff3d465558bc1a6d6bbd62827a719 100755 --- a/auth/oauth2/facebook_token.php +++ b/auth/oauth2/facebook_token.php @@ -22,9 +22,12 @@ * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +/* Facebook callback page */ + include '../../include/init.php'; startSession(); +// Retrieve Facebook configuration $Facebook = $AUTHENTICATION_METHODS['Facebook']; $fb = new Facebook\Facebook([ @@ -80,9 +83,11 @@ $fbUser = $response->getGraphUser(); $typedId = $fbUser["id"]; +// Search if the user is already registered into RAP using the Facebook ID. $user = $userHandler->findUserByIdentity(RAP\Identity::FACEBOOK, $typedId); if ($user === null) { + // Create new user $user = new RAP\User(); $identity = new RAP\Identity(RAP\Identity::FACEBOOK); diff --git a/auth/oauth2/google_token.php b/auth/oauth2/google_token.php index 8f056d5c5d17e0d88946f3dc0e6e426bf6c5274d..c5a5bd43c175baf3abfa8b5e03cd7c193be1e7d0 100644 --- a/auth/oauth2/google_token.php +++ b/auth/oauth2/google_token.php @@ -22,9 +22,12 @@ * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +/* Google redirect and callback page */ + include '../../include/init.php'; startSession(); +// Retrieve Google configuration $Google = $AUTHENTICATION_METHODS['Google']; $client = new Google_Client(array( @@ -53,7 +56,7 @@ if (isset($_GET['code'])) { if ($client->getAccessToken()) { - // Query web service + // Query web service for retrieving user information $service = new Google_Service_People($client); try { @@ -74,9 +77,11 @@ if ($client->getAccessToken()) { $typedId = explode('/', $res->getResourceName())[1]; + // Search if the user is already registered into RAP using the Google ID. $user = $userHandler->findUserByIdentity(RAP\Identity::GOOGLE, $typedId); if ($user === null) { + // Create new user $user = new RAP\User(); $identity = new RAP\Identity(RAP\Identity::GOOGLE); diff --git a/auth/oauth2/linkedin_login.php b/auth/oauth2/linkedin_login.php index df940e81b57d84b22a99132e701924b203e30d74..2c64969c6e03d78549090327a7af3cc67fa5b61a 100644 --- a/auth/oauth2/linkedin_login.php +++ b/auth/oauth2/linkedin_login.php @@ -22,9 +22,12 @@ * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +/* This page redirects to LinkedIn login page */ + include '../../include/init.php'; startSession(); +// Retrieve LinkedIn configuration $LinkedIn = $AUTHENTICATION_METHODS['LinkedIn']; $url = "https://www.linkedin.com/oauth/v2/authorization?response_type=code"; diff --git a/auth/oauth2/linkedin_token.php b/auth/oauth2/linkedin_token.php index e38700ab76a7e4f19e32988de46ffe62936d3374..fde3cb8c203e2364e7241ec2d677b0255e5986bf 100644 --- a/auth/oauth2/linkedin_token.php +++ b/auth/oauth2/linkedin_token.php @@ -22,9 +22,12 @@ * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +/* LinkedIn callback page. Curl is used, because LinkedIn doesn't provide official PHP API. */ + include '../../include/init.php'; startSession(); +// Retrieve LinkedIn configuration $LinkedIn = $AUTHENTICATION_METHODS['LinkedIn']; if (!isset($_REQUEST['code'])) { @@ -100,9 +103,11 @@ if ($info2['http_code'] === 200) { $typedId = $data['id']; + // Search if the user is already registered into RAP using the LinkedIn ID. $user = $userHandler->findUserByIdentity(RAP\Identity::LINKEDIN, $typedId); if ($user === null) { + // Create new user $user = new RAP\User(); $identity = new RAP\Identity(RAP\Identity::LINKEDIN); diff --git a/auth/saml2/aai.php b/auth/saml2/aai.php index 59723e06f1b1a441606f7be8c9e11e05a8d1f173..cd89610c0b7fe03824153addadeb37c215e77b99 100644 --- a/auth/saml2/aai.php +++ b/auth/saml2/aai.php @@ -22,16 +22,29 @@ * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +/* This page MUST be protected by Shibboleth authentication + * On Apache httpd: + * AuthType shibboleth + * ShibRequestSetting requireSession 1 + * Require valid-user + */ + include '../../include/init.php'; startSession(); if (isset($_SERVER['Shib-Session-ID'])) { + // Retrieving eduPersonPrincipalName (eppn) $eppn = $_SERVER['eppn']; + // Search if the user is already registered into RAP using the eppn. + // The persistent id should be a more appropriate identifier, however at IA2 + // we need to import all INAF user into RAP, even if they will never register, + // and in that case we know only their eppn. $user = $userHandler->findUserByIdentity(RAP\Identity::EDU_GAIN, $eppn); if ($user === null) { + // Creating a new user $user = new RAP\User(); $identity = new RAP\Identity(RAP\Identity::EDU_GAIN); diff --git a/auth/x509/certlogin.php b/auth/x509/certlogin.php index ba9c20e807b0c6c549994b2cd2000f9524e96b2d..61085fe22ce60690a405dbf0db00c3c87900dd73 100644 --- a/auth/x509/certlogin.php +++ b/auth/x509/certlogin.php @@ -22,6 +22,12 @@ * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +/* This page must be protected by client certificate authentication + * On Apache httpd: + * SSLVerifyClient require + * SSLVerifyDepth 10 + * SSLOptions +ExportCertData + */ include '../../include/init.php'; startSession(); @@ -44,10 +50,20 @@ function saveUserFromX509Data($x509Data) { $userHandler->saveUser($user); $session->x509DataToRegister = null; + $session->save(); return $user; } +/** + * We want to extract name and surname from the X.509 certificate, however X.509 + * puts name and surname together (inside the CN field). + * If name and surname are single words it is possible to retrieve them splitting + * on the space character, otherwise the user has to choose the correct combination. + * In that case partial X.509 data is temporarily stored into the user session and + * the page views/x509-name-surname.php is shown to the user before completing the + * registration, in order to allow him/her selecting the correct name and surname. + */ if ($session->x509DataToRegister !== null && $session->x509DataToRegister->name !== null) { $user = saveUserFromX509Data($session->x509DataToRegister); diff --git a/classes/CallbackHandler.php b/classes/CallbackHandler.php index 44840615d1e9525303afce2ef40d5cc9f4ce149b..dfd6c5d935ea772fb6bbe867ec4ad96615fbf7a4 100644 --- a/classes/CallbackHandler.php +++ b/classes/CallbackHandler.php @@ -24,6 +24,9 @@ namespace RAP; +/** + * Manage callback URL validation and redirection + */ class CallbackHandler { private $dao; @@ -49,7 +52,11 @@ class CallbackHandler { } /** - * returns null if the callback URL is not listed in configuration file. + * Each callback has a title and a logo in order to avoid confusion in users + * and show in which application they are logging in using RAP. + * @param type $callbackURL + * @return type the callback title or null if the callback URL is not listed + * in configuration file or it doesn't have a title. */ public function getCallbackTitle($callbackURL) { @@ -62,6 +69,13 @@ class CallbackHandler { return null; } + /** + * Each callback has a title and a logo in order to avoid confusion in users + * and show in which application they are logging in using RAP. + * @param type $callbackURL + * @return type the callback logo or null if the callback URL is not listed + * in configuration file or it doesn't have a logo. + */ public function getCallbackLogo($callbackURL) { foreach ($this->callbacks as $callback) { diff --git a/classes/DAO.php b/classes/DAO.php index 9a28d92f582d5dd429beb67264522c8941364b2b..c5d5039b41e0661053affd93f8f5f9e64ea9edd3 100644 --- a/classes/DAO.php +++ b/classes/DAO.php @@ -24,46 +24,104 @@ namespace RAP; +/** + * Data Access Object interface for accessing the RAP database. + * Current implementations: RAP\MySQLDAO + */ interface DAO { + /** + * @return type PDO object for accessing the database + */ function getDBHandler(); + /** + * Store a new login token into the database. + * @param type $token login token + * @param type $userId + */ function createLoginToken($token, $userId); + /** + * Retrieve the user ID from the login token. + * @param type $token + * @return type user ID + */ function findLoginToken($token); + /** + * Delete a login token from the database. This happens when the caller + * application has received the token and used it for retrieving user + * information from the token using the RAP REST web service. + * @param type $token login token + */ function deleteLoginToken($token); /** - * Return the new identity ID. + * Create a new identity. + * @param type $userId the user ID associated to that identity + * @return type the new identity ID */ function insertIdentity(Identity $identity, $userId); /** - * Return the new user ID. + * Create a new user. + * @return the new user ID. */ function createUser(); + /** + * @return RAP\User an user object, null if nothing was found. + */ function findUserById($userId); function setPrimaryIdentity($userId, $identityId); /** - * Return a User object, null if nothing was found. + * Retrieve the user associated to a given identity using the typedId. * @param type $type Identity type (EDU_GAIN, X509, GOOGLE, ...) - * @param type $identifier value used to search the identity in the database + * @param type $typedId typed unique value used to search the identity in the database + * @return RAP\User an user object, null if nothing was found. */ - function findUserByIdentity($type, $identifier); + function findUserByIdentity($type, $typedId); + /** + * Retrieve a set of users matching a given search text. + * @param type $searchText name, surname or email + * @return list of RAP\User objects + */ function searchUser($searchText); + /** + * Store into the database information about a new join request. + * @param type $token join token + * @param type $applicantUserId the user asking for the join + * @param type $targetUserId the user target of the join + */ function createJoinRequest($token, $applicantUserId, $targetUserId); + /** + * Retrieve join request information. + * @param type $token join token + * @return an array of 2 elements having the applicant user id at the first + * position and the target user id at the second position; null if nothing + * was found. + * @throws Exception if multiple requests has been found for the same token. + */ function findJoinRequest($token); - function deleteUser($userId); - + /** + * Perform a join request. + * @param type $userId1 the user that will receive all identities + * @param type $userId2 the user that will lost the identities and will be + * deleted from the database + */ function joinUsers($userId1, $userId2); + /** + * When a join action is performed the join request data (join token and user + * identifiers) needs to be removed from the database. + * @param type $token join token + */ function deleteJoinRequest($token); } diff --git a/classes/Identity.php b/classes/Identity.php index 500a1aafe2be3183dc3f1c0bd013e607433d34bd..7710595fe534008d4fc00b0cdfb05c53971fb6cc 100644 --- a/classes/Identity.php +++ b/classes/Identity.php @@ -24,6 +24,9 @@ namespace RAP; +/** + * Data model for identities. + */ class Identity { const EDU_GAIN = "eduGAIN"; @@ -31,9 +34,8 @@ class Identity { const GOOGLE = "Google"; const FACEBOOK = "Facebook"; const LINKEDIN = "LinkedIn"; - const LOCAL = "Local"; - private static $ALLOWED_TYPES = [Identity::EDU_GAIN, Identity::X509, Identity::GOOGLE, Identity::FACEBOOK, Identity::LINKEDIN, Identity::LOCAL]; + private static $ALLOWED_TYPES = [Identity::EDU_GAIN, Identity::X509, Identity::GOOGLE, Identity::FACEBOOK, Identity::LINKEDIN]; /** * Identity id in the database. Mandatory field. @@ -46,23 +48,27 @@ class Identity { public $type; /** - * Data related to specific account type (shibboleth persistent id, facebook id, etc, ...). Mandatory field. + * Data related to specific account type (shibboleth persistent id, + * facebook id, certificate serial number, etc, ...). Mandatory field. */ public $typedId; /** * Primary email related to this identity. Mandatory field. - * User can have additional email addresses. These are stored into User class. + * @todo Maybe an user can have additional email addresses (e.g.: Google + * API provides them). Should we store them somewhere? */ public $email; /** - * First name + * First name. + * This should be mandatory, however for old IA2 users we have only email address. */ public $name; /** * Last name / Family name + * This should be mandatory, however for old IA2 users we have only email address. */ public $surname; @@ -73,6 +79,9 @@ class Identity { /** * For eduGAIN identities. + * This is currently the same as the typedId for eduGAIN identity types, because + * at IA2 we need this (see the wiki). Use the Shibboleth persistent id should + * be a more appropriate typedId for these cases. */ public $eppn; diff --git a/classes/MailSender.php b/classes/MailSender.php index 67742f43c7330f6abadf9ed19a149c8ced4c08b9..deca8f7bf27fe4efef888a98cf1fe278a0fdc6ed 100644 --- a/classes/MailSender.php +++ b/classes/MailSender.php @@ -24,6 +24,10 @@ namespace RAP; +/** + * Manage mail sending. + * Currently used only for join email messages. + */ class MailSender { private $serverName; @@ -34,6 +38,13 @@ class MailSender { $this->basePath = $basePath; } + /** + * Send the email for confirming the join request. + * @param \RAP\User $recipientUser user target of the join requests: he/she + * will receive the email containing the confirmation link + * @param \RAP\User $applicantUser user that have requested the join + * @param string $token the join token + */ public function sendJoinEmail(User $recipientUser, User $applicantUser, $token) { $subject = "IA2 RAP: Join request"; diff --git a/classes/MySQLDAO.php b/classes/MySQLDAO.php index 03b492c7fe713d4daa83b04f0e915ddd78ced404..c139a0587e0bdd1c6160b0959f00eff4f66df859 100644 --- a/classes/MySQLDAO.php +++ b/classes/MySQLDAO.php @@ -26,6 +26,9 @@ namespace RAP; use PDO; +/** + * MySQL implementation of the DAO interface. See comments on the DAO interface. + */ class MySQLDAO implements DAO { private $config; @@ -289,14 +292,6 @@ class MySQLDAO implements DAO { } } - public function deleteUser($userId) { - $dbh = $this->getDBHandler(); - - $stmt3 = $dbh->prepare("DELETE FROM TABLE `user` WHERE `id` = :id2"); - $stmt3->bindParam(':id2', $userId); - $stmt3->execute(); - } - public function joinUsers($userId1, $userId2) { $dbh = $this->getDBHandler(); diff --git a/classes/SessionData.php b/classes/SessionData.php index 4b365a84254964525e4ba50a57a80cae9bbf4517..294e934230c1aebe482a566c4c7e474205c716fa 100644 --- a/classes/SessionData.php +++ b/classes/SessionData.php @@ -24,6 +24,10 @@ namespace RAP; +/** + * This class wraps various objects that need to be stored into the session in + * order to provide an object oriented transparent session management. + */ class SessionData { private $dao; @@ -34,14 +38,26 @@ class SessionData { public $userSearchResults; public $x509DataToRegister; + /** + * @todo: move DAO away from here + */ public function __construct(DAO $dao) { $this->dao = $dao; } + /** + * Store the data into the $_SESSION PHP variable + */ public function save() { $_SESSION['SessionData'] = $this; } + /** + * Retrieve the SessionData object from the $_SESSION PHP variable. Create a + * new one if it is necessary. + * @param \RAP\DAO $dao + * @return \RAP\SessionData the SessionData object + */ public static function get(DAO $dao) { if (!isset($_SESSION['SessionData'])) { @@ -70,6 +86,12 @@ class SessionData { return $this->callbackLogo; } + /** + * Perform a user search and store the results inside the session. This is + * used for achieving the user selection using the dropdown menu in the join + * request modal. + * @param string $searchText + */ public function searchUser($searchText) { $users = $this->dao->searchUser($searchText); @@ -85,6 +107,12 @@ class SessionData { $this->save(); } + /** + * Update the user data model stored into the session after the primary + * identity has changed, in order to avoid reading again the user data from + * the database. + * @param int $identityId + */ public function updatePrimaryIdentity($identityId) { foreach ($this->user->identities as $identity) { $identity->primary = ($identity->id === $identityId); diff --git a/classes/User.php b/classes/User.php index e82db49d2f42c4a707db4eee35aa32fbbf02f82f..21967ff717e1ab17a1dccb49f73912a5c3072d5b 100644 --- a/classes/User.php +++ b/classes/User.php @@ -24,9 +24,14 @@ namespace RAP; +/** + * Data model for the user. An user is a set of identities. + */ class User { + // User ID public $id; + // List of identities public $identities; public function __construct() { @@ -43,6 +48,7 @@ class User { return $identity->email; } } + // A primary identity MUST be defined throw new \Exception("No primary identity defined for user " . $this->id); } diff --git a/classes/UserHandler.php b/classes/UserHandler.php index 5f1d52d9ce225ef9ac9b7d05e82417698c6c95e2..4614a8f17b5004b7552c16e2a60eda7d56f8a76b 100644 --- a/classes/UserHandler.php +++ b/classes/UserHandler.php @@ -24,6 +24,9 @@ namespace RAP; +/** + * Perform operations on users. + */ class UserHandler { private $dao; @@ -34,6 +37,11 @@ class UserHandler { $this->grouperConfig = $grouperConfig; } + /** + * Update user information into the database, creating a new user or adding + * new identities to it. + * @param \RAP\User $user + */ public function saveUser(User $user) { $primarySpecified = true; @@ -60,6 +68,11 @@ class UserHandler { return $this->dao->findUserByIdentity($type, $identifier); } + /** + * Build an URL for the web service endpoint that needs to be called in order + * to move groups from one user to the other during a join operation. + * @return string grouper URL for the PrepareToJoinServlet + */ private function getJoinURL() { $joinURL = $this->grouperConfig['wsURL']; @@ -73,6 +86,7 @@ class UserHandler { public function joinUsers($userId1, $userId2) { + // Call Grouper for moving groups and privileges from one user to the other if ($this->grouperConfig !== null) { //create cURL connection @@ -103,6 +117,7 @@ class UserHandler { } } + // Call DAO for performing join operation into the RAP database. $this->dao->joinUsers($userId1, $userId2); } diff --git a/classes/UserSearchResult.php b/classes/UserSearchResult.php index 685abc61b1932480412416e60b98f36b03cec843..c1d49d90c8284a12229685fddb6ba6e704b10f86 100644 --- a/classes/UserSearchResult.php +++ b/classes/UserSearchResult.php @@ -24,9 +24,19 @@ namespace RAP; +/** + * Data model representing an item of the result of an user search. This is used + * in order to display the dropdown menu for user selection in the join modal + * dialog avoiding exposing the user identifiers in the AJAX calls. + * This data is stored into a list inside the SessionData object. The user will + * select one of the item by index and not by the user ID. + */ class UserSearchResult { + // The user object is wrapped by this class and hidden to AJAX. private $user; + // Only this text is returned to the AJAX call. + // See gui-backend.php (/user?search endpoint) public $userDisplayText; public static function buildFromUser(User $user) { diff --git a/classes/Util.php b/classes/Util.php index 9a786b1a9f4aba5a7a057cc9250043daee978504..4879b9b08d3005625644a1736b2202b030093860 100644 --- a/classes/Util.php +++ b/classes/Util.php @@ -29,6 +29,9 @@ namespace RAP; */ class Util { + /** + * @return string random string + */ public static function createNewToken() { // Credits: http://stackoverflow.com/a/18890309/771431 return bin2hex(openssl_random_pseudo_bytes(16)); diff --git a/classes/X509Data.php b/classes/X509Data.php index 3c774111bd090605597f182aa384b3653eaf489e..bede4cdc4a9fa7e844fd53d51e49e5dbb65b73e5 100644 --- a/classes/X509Data.php +++ b/classes/X509Data.php @@ -24,18 +24,27 @@ namespace RAP; +/** + * Parse X.509 certificate extracting serial number, email, name, surname and institution. + * Because the certificate puts name and surname together, when name and surname are + * composed by more than two words this class returns a partial result containing + * possible alternatives. + */ class X509Data { public $email; - public $fullName; public $institution; public $serialNumber; + // name(s) and surname, space separated + public $fullName; public $name; public $surname; + // list of possible names: this is populated when fullName has more than two words public $candidateNames; /** - * Retrieve full name from CN, removing the e-mail if necessary + * Retrieve full name of the person (name+surname) from CN, + * removing the e-mail if necessary. */ private function parseCN($cn) { $cnSplit = explode(" ", $cn); @@ -64,6 +73,10 @@ class X509Data { } } + /** + * Extract data in a simpler way using openssl_x509_parse PHP function. + * @param type $sslClientCert $_SERVER['SSL_CLIENT_CERT'] + */ private function parseUsingOpenSSL($sslClientCert) { $parsedX509 = openssl_x509_parse($sslClientCert); @@ -85,6 +98,8 @@ class X509Data { } /** + * The serial number is too big to be converted from hex to dec using the + * builtin hexdec function, so BC Math functions are used. * Credits: https://stackoverflow.com/a/1273535/771431 */ private static function bchexdec($hex) { @@ -96,6 +111,9 @@ class X509Data { return $dec; } + /** + * Populate name and surname or candidateNames variables + */ private function fillNameAndSurnameOrCandidates() { $nameSplit = explode(' ', $this->fullName); @@ -117,12 +135,25 @@ class X509Data { } } + /** + * This function is called when the user select the correct candidate name. + * Surname is calculated as the remaining string. + * @param type $candidateNameIndex the index of the selected element into + * the candidateNames list + */ public function selectCandidateName($candidateNameIndex) { $candidateName = $this->candidateNames[$candidateNameIndex]; $this->name = $candidateName; $this->surname = substr($this->fullName, strlen($candidateName) + 1); } + /** + * Extract client certificate data needed by RAP from PHP $_SERVER variable. + * @param type $server PHP $_SERVER variable. This is passed as parameter in order + * to make class testable (mocking the $_SERVER variable). Tests have not + * been written yet. + * @return \RAP\X509Data + */ public static function parse($server) { $parsedData = new X509Data(); diff --git a/config-example.php b/config-example.php index 8a66713140f79f84e89dce469264044cdf1a61b1..d9bf364413b12f053284be5c7ea233c8111eff5a 100644 --- a/config-example.php +++ b/config-example.php @@ -23,7 +23,7 @@ */ $CONTEXT_ROOT = "/rap-ia2"; -$VERSION = "1.0.1"; +$VERSION = "1.0.2"; $PROTOCOL = stripos($_SERVER['SERVER_PROTOCOL'], 'https') ? 'https://' : 'http://'; $BASE_PATH = $PROTOCOL . $_SERVER['HTTP_HOST'] . $CONTEXT_ROOT; diff --git a/css/style.css b/css/style.css index bdc99e9b4961043543d61226d7dd80977a5640db..021218b894fe81a217d6e3e322c0b01bc07b31b1 100644 --- a/css/style.css +++ b/css/style.css @@ -3,6 +3,7 @@ body { padding-bottom: 150px; } +/* Waiting div overlay */ .waiting { position: fixed; top: 0; @@ -27,6 +28,7 @@ body { vertical-align: middle; } +/* CSS animation for authentication method buttons in the RAP main page. */ @keyframes home_pulse { from { transform: scale(1, 1); @@ -37,6 +39,7 @@ body { } } +/* Box containing one or more authentication methods in the RAP main page. */ .home-box { display: inline-block; width: 240px; diff --git a/include/front-controller.php b/include/front-controller.php index 202806fc2992507d891b7834e9a2db5513805265..2d1c4056a88654aefad96b8d6e67b6ebac80a566 100644 --- a/include/front-controller.php +++ b/include/front-controller.php @@ -17,18 +17,24 @@ function setCallback($callback) { return $session->getCallbackURL(); } +/** + * Display the main page (authentication method selection) or the available + * services list if a valid callback is not found + */ Flight::route('/', function() { startSession(); $callback = setCallback(Flight::request()->data['callback']); - global $session, $callbackHandler, $BASE_PATH, $AUTHENTICATION_METHODS; + global $session, $callbackHandler, $BASE_PATH, $AUTHENTICATION_METHODS, $VERSION; if ($callback === null && $session->user === null) { Flight::render('services-list.php', array('title' => 'RAP', + 'version' => $VERSION, 'action' => $BASE_PATH . '/')); } else if ($callback !== null && $session->user !== null) { $redirectURL = $callbackHandler->getLoginWithTokenURL($session->user->id, $callback); Flight::redirect($redirectURL); } else { Flight::render('index.php', array('title' => 'RAP', + 'version' => $VERSION, 'session' => $session, 'auth' => $AUTHENTICATION_METHODS)); } }); @@ -72,6 +78,9 @@ Flight::route('/direct', function() { sendAuthRedirect($AUTHENTICATION_METHODS['DirectIdP']['url']); }); +/** + * Render the join confirmation page (confirmation link received by email). + */ Flight::route('GET /confirm-join', function() { $token = Flight::request()->query['token']; @@ -80,7 +89,7 @@ Flight::route('GET /confirm-join', function() { die("Token not found"); } - global $dao; + global $dao, $VERSION; $userIds = $dao->findJoinRequest($token); if ($userIds === null) { @@ -92,11 +101,15 @@ Flight::route('GET /confirm-join', function() { $targetUser = $dao->findUserById($userIds[1]); Flight::render('confirm-join.php', array('title' => 'RAP', + 'version' => $VERSION, 'token' => $token, 'applicantUser' => $applicantUser, 'targetUser' => $targetUser)); }); +/** + * Confirm a join and show the page containing the operation status. + */ Flight::route('POST /confirm-join', function() { global $dao, $userHandler, $auditLog; @@ -123,18 +136,24 @@ Flight::route('POST /confirm-join', function() { session_start(); session_destroy(); - global $BASE_PATH; + global $BASE_PATH, $VERSION; Flight::render('join-success.php', array('title' => 'Success - RAP Join Request', + 'version' => $VERSION, 'basePath' => $BASE_PATH)); }); +/** + * Render the page for selecting th correct name and username from candidates + * list during a X.509 registration. + */ Flight::route('GET /x509-name-surname', function() { startSession(); - global $session, $BASE_PATH; + global $session, $BASE_PATH, $VERSION; if ($session->x509DataToRegister !== null && $session->x509DataToRegister->name === null) { Flight::render('x509-name-surname.php', array('title' => 'Select name and surname', + 'version' => $VERSION, 'fullName' => $session->x509DataToRegister->fullName, 'candidateNames' => $session->x509DataToRegister->candidateNames)); } else { @@ -143,6 +162,10 @@ Flight::route('GET /x509-name-surname', function() { } }); +/** + * Complete the X.509 registration selecting the correct name and surname specified + * by the user. + */ Flight::route('POST /submit-x509-name', function() { $selectedNameIndex = Flight::request()->data['selected-name']; diff --git a/include/gui-backend.php b/include/gui-backend.php index d29b905fe532b7216657c4a4c9e8b60dbffc7eae..44e3eda9384dd4bb5aa03b63734d7cdda528a41d 100644 --- a/include/gui-backend.php +++ b/include/gui-backend.php @@ -1,7 +1,7 @@ <?php /** - * REST backend for JavaScript code. + * REST backend for JavaScript code (AJAX calls). */ // @@ -16,6 +16,9 @@ function checkSession() { } } +/** + * This is called when an user search for other users into the join modal dialog. + */ Flight::route('GET /user', function() { checkSession(); @@ -48,17 +51,23 @@ Flight::route('POST /join', function() { echo $selectedSearchResult->userDisplayText; }); - +/** + * Set the primary identity based on the index of the identity clicked by the + * user on the Account Management page. We MUST use the index and not directly + * the identity id for security reason (one can use the browser developer tools + * for calling the AJAX call with an arbitrary number). + */ Flight::route('POST /primary-identity', function() { checkSession(); global $session, $dao; - $identityId = Flight::request()->data['id']; + $identityIndex = intval(Flight::request()->data['index']); + $identityId = $session->user->identities[$identityIndex]->id; $dao->setPrimaryIdentity($session->user->id, $identityId); $session->updatePrimaryIdentity($identityId); - + // Following variable is used to render user-data $user = $session->user; include 'user-data.php'; diff --git a/include/header.php b/include/header.php index 64d7268d0d0939ebddd610122c06aef2eb30d0e3..267bb9c942bcf496ee4e529208a2aae0d9e65ed8 100644 --- a/include/header.php +++ b/include/header.php @@ -9,7 +9,7 @@ <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script> <link rel="stylesheet" href="css/style.css?v=2" /> <link rel="stylesheet" href="css/animation.css?v=2" /> - <script src="js/script.js"></script> + <script src="js/script.js?v=<?php echo $version; ?>"></script> </head> <body> <header id="main-header"> diff --git a/include/init.php b/include/init.php index 568565e699046faa0de1d013862ef8d1cbf22945..67fa2ba5683762c9f9005fa1797331d2c873501d 100644 --- a/include/init.php +++ b/include/init.php @@ -22,6 +22,10 @@ * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +/** + * Initialization file called by all the other pages. + * Probably some global variables should be removed from here. + */ define('ROOT', dirname(dirname(__FILE__))); // Defining autoload for RAP classes diff --git a/include/rest-web-service.php b/include/rest-web-service.php index a3d507d3526a328f04dc44f3578cbbb8969a613a..b69161a3d869f6f90d0c2906453f3f3cef9719dd 100644 --- a/include/rest-web-service.php +++ b/include/rest-web-service.php @@ -2,10 +2,23 @@ /** * REST Web Service using http://flightphp.com/ + * This Web Service should be called only by authorized applications external to + * RAP, so this directory should be password protected. + * + * Apache configuration: + * <Location "/rap-ia2/ws"> + * AuthType basic + * AuthName RAP + * AuthUserFile rap-service-passwd + * Require valid-user + * </Location> */ // $WS_PREFIX = '/ws'; +/** + * Retrieve user information from login token. + */ Flight::route('GET ' . $WS_PREFIX . '/user-info', function() { global $dao; @@ -24,6 +37,9 @@ Flight::route('GET ' . $WS_PREFIX . '/user-info', function() { echo $userData; }); +/** + * Retrieve user information from user ID. + */ Flight::route('GET ' . $WS_PREFIX . '/user/@userId', function($userId) { global $dao; @@ -38,6 +54,9 @@ Flight::route('GET ' . $WS_PREFIX . '/user/@userId', function($userId) { } }); +/** + * Search users from search text (name, surname, email). + */ Flight::route('GET ' . $WS_PREFIX . '/user', function() { global $dao; @@ -48,7 +67,7 @@ Flight::route('GET ' . $WS_PREFIX . '/user', function() { }); /** - * Create new user from identity data. Return the new user. + * Create new user from identity data. Return the new user encoded in JSON. */ Flight::route('POST ' . $WS_PREFIX . '/user', function() { @@ -82,6 +101,9 @@ Flight::route('POST ' . $WS_PREFIX . '/user', function() { echo json_encode($user); }); +/** + * Perform a join. + */ Flight::route('POST ' . $WS_PREFIX . '/join', function() { global $userHandler; diff --git a/include/user-data.php b/include/user-data.php index 37c05e194da56aa68d9096dd50caa3795b30a87f..87df4a1dff27a7b5b4bc8cad4507e2f33b80700f 100644 --- a/include/user-data.php +++ b/include/user-data.php @@ -1,39 +1,49 @@ -<?php foreach ($user->identities as $identity) { ?> +<?php +/** + * This fragment represent a panel containing information about an user and its + * set of identities. + */ +$i = 0; // identity index +foreach ($user->identities as $identity) { + ?> <dl class="dl-horizontal"> <dt> <?php if (!isset($readOnly)) { ?> - <?php if ($identity->primary) { ?> + <?php if ($identity->primary) { ?> <span class="primary-identity-icon" data-toggle="tooltip" data-placement="left" title="This is your primary identity. You will receive email messages on the address related to this identity."> <span class="glyphicon glyphicon-star"></span> </span> - <?php } else { ?> + <?php } else { ?> <span class="primary-identity-icon" data-toggle="tooltip" data-placement="left" title="Click on this icon to set this as the primary identity"> - <a href="#" onclick="setPrimaryIdentity(<?php echo $identity->id; ?>);"> + <a href="#" onclick="setPrimaryIdentity(<?php echo $i; ?>);"> <span class="glyphicon glyphicon-star-empty"></span> </a> </span> <?php } ?> - <?php } ?> + <?php } ?> Type </dt> <dd><?php echo $identity->getUIType(); ?></dd> <dt>E-mail</dt> <dd><?php echo $identity->email; ?></dd> - <?php if ($identity->eppn !== null) { ?> + <?php if ($identity->eppn !== null) { ?> <dt><abbr title="EduPerson Principal Name, an unique identifier used into federations.">EPPN</abbr></dt> <dd><?php echo $identity->eppn; ?></dd> <?php } ?> - <?php if ($identity->name !== null) { ?> + <?php if ($identity->name !== null) { ?> <dt>Name</dt> <dd><?php echo $identity->name; ?></dd> <?php } ?> - <?php if ($identity->surname !== null) { ?> + <?php if ($identity->surname !== null) { ?> <dt>Surname</dt> <dd><?php echo $identity->surname; ?></dd> <?php } ?> - <?php if ($identity->institution !== null) { ?> + <?php if ($identity->institution !== null) { ?> <dt>Institution</dt> <dd><?php echo $identity->institution; ?></dd> - <?php } ?> + <?php } ?> </dl> -<?php } ?> + <?php + $i++; +} +?> diff --git a/js/index.js b/js/index.js index a55d573df947a3c6262da841bc5a46520f3e69fb..800f74f843ba519dceb921bd400100dcf046c1d9 100644 --- a/js/index.js +++ b/js/index.js @@ -1,6 +1,14 @@ +/** + * JS code included only in index.php page. + */ +// IIFE for keeping private functions and variables inside. (function ($) { - // function factory used to generate function to be executed at timeout (see below) + /** + * Function factory used to generate function to be executed at timeout (see below) + * @param {string} searchText + * @returns {Function} + */ function searchUserFactory(searchText) { return function () { $.get('user?search=' + searchText, function (response) { @@ -19,6 +27,10 @@ }; } + /** + * Function associated to the "Send join request" button on the join modal + * dialog. + */ function sendJoinRequest() { $userSelector = $('#user-selector-group select'); @@ -34,16 +46,27 @@ } } - window.setPrimaryIdentity = function (identityId) { + /** + * Select the primary identity from the available identities of the user. + * @param {int} index + */ + window.setPrimaryIdentity = function (index) { $.post('primary-identity', { - id: identityId + index: index }, function (response) { $('#panel-identities .panel-body').html(response); - // restore tooltips + // restore tooltips (JS event handlers are deleted when DOM element + // is replaced). loadTooltips(); }); }; + /** + * Initialize Bootstrap tooltips. + * As specified into Bootstrap documentation "For performance reasons, the + * Tooltip and Popover data-apis are opt-in, meaning you must initialize + * them yourself." + */ function loadTooltips() { $('.primary-identity-icon').tooltip(); } diff --git a/js/script.js b/js/script.js index 744088f5a496bb7552150503c5872c39dcbc71e8..3435cc6c02c50786ca792c2c91ea37d618f6fff5 100644 --- a/js/script.js +++ b/js/script.js @@ -1,3 +1,6 @@ +/** + * Common scripts. + */ (function ($) { // Loading/waiting animation @@ -8,6 +11,7 @@ $('.waiting').addClass('hide'); }; + // Showing loading animation during AJAX calls. $(document).ajaxStart(showWaiting); $(document).ajaxStop(hideWaiting); diff --git a/js/x509-name-surname.js b/js/x509-name-surname.js index fb1488295003e7ec0d0a05642fcb9d0e9fbcbff7..771b62e55206abee49fe7920e0e451c47462b2b3 100644 --- a/js/x509-name-surname.js +++ b/js/x509-name-surname.js @@ -1,10 +1,20 @@ +/** + * JavaScript necessary for the page views/x509-name-surname.php + */ (function ($) { var _fullName; + /** + * @param {string} fullName: name+surname + */ window.initPage = function (fullName) { _fullName = fullName; }; + /** + * Calculate the surname removing the selected candidate name from the full + * name. + */ function fillSurnameFromSelectedCandidateName() { var selectedName = $('#name-selector option:selected').text().trim(); var surname = _fullName.substring(selectedName.length + 1); @@ -13,6 +23,7 @@ // When the document is loaded $(document).ready(function () { + // Add event handler on the name-selector dropdown menu $(document).on('change', '#name-selector', fillSurnameFromSelectedCandidateName); fillSurnameFromSelectedCandidateName(); }); diff --git a/views/index.php b/views/index.php index c667d11d193c9710181fc9e1ef24bd1c623daccd..ebcc6d96c47ac5b80f4f3c2e137ca99b7a99a938 100644 --- a/views/index.php +++ b/views/index.php @@ -1,7 +1,7 @@ <?php include 'include/header.php'; ?> -<script src="js/index.js"></script> +<script src="js/index.js?v=<?php echo $version; ?>"></script> <?php if ($session->user === null) { ?> <div class="row"> diff --git a/views/services-list.php b/views/services-list.php index 800bff9654125da92247db2bed6a87747f2668d3..f74215ce00e839b8c09ca32526d3d97efacf8e05 100644 --- a/views/services-list.php +++ b/views/services-list.php @@ -1,4 +1,7 @@ <?php +/** + * This page is specific for IA2. + */ include 'include/header.php'; ?> <div class="col-sm-offset-2 col-sm-10 services-list-wrapper"> diff --git a/views/x509-name-surname.php b/views/x509-name-surname.php index aa5f159e70efbb4fc81d87cd5cc7ae1b8847bfe6..2b7080849005c10e8f6fc7eb33a08efc9d0a8285 100644 --- a/views/x509-name-surname.php +++ b/views/x509-name-surname.php @@ -1,7 +1,7 @@ <?php include 'include/header.php'; ?> -<script src="js/x509-name-surname.js"></script> +<script src="js/x509-name-surname.js?v=<?php echo $version; ?>"></script> <script>initPage('<?php echo $fullName; ?>');</script> <h1 class="text-center page-title">Name and surname selection</h1>