* @author Romain Quetiez * @author Denis Flaven * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL */ require_once(APPROOT."/application/nicewebpage.class.inc.php"); /** * Web page used for displaying the login form */ class LoginWebPage extends NiceWebPage { public function __construct() { parent::__construct("iTop Login"); $this->add_style(<<add_header('WWW-Authenticate: Basic realm="'.Dict::Format('UI:iTopVersion:Short', ITOP_VERSION)); $this->add_header('HTTP/1.0 401 Unauthorized'); // Note: displayed when the user will click on Cancel $this->add('

'.Dict::S('UI:Login:Error:AccessRestricted').'

'); break; case 'external': case 'form': default: // In case the settings get messed up... $sAuthUser = utils::ReadParam('auth_user', '', true, 'raw_data'); $sAuthPwd = utils::ReadParam('suggest_pwd', '', true, 'raw_data'); $sVersionShort = Dict::Format('UI:iTopVersion:Short', ITOP_VERSION); $this->add("
\n"); $this->add("
\n"); $this->add("

".Dict::S('UI:Login:Welcome')."

\n"); if ($bFailedLogin) { $this->add("

".Dict::S('UI:Login:IncorrectLoginPassword')."

\n"); } else { $this->add("

".Dict::S('UI:Login:IdentifyYourself')."

\n"); } $this->add("
\n"); $this->add("\n"); $this->add("\n"); $this->add("\n"); $this->add("\n"); $this->add("
\n"); $this->add("\n"); $this->add("
\n"); $this->add("
\n"); break; } } public function DisplayChangePwdForm($bFailedLogin = false) { $sAuthUser = utils::ReadParam('auth_user', '', false, 'raw_data'); $sVersionShort = Dict::Format('UI:iTopVersion:Short', ITOP_VERSION); $sInconsistenPwdMsg = Dict::S('UI:Login:RetypePwdDoesNotMatch'); $this->add_script(<<add("
\n"); $this->add("
\n"); $this->add("

".Dict::S('UI:Login:ChangeYourPassword')."

\n"); if ($bFailedLogin) { $this->add("

".Dict::S('UI:Login:IncorrectOldPassword')."

\n"); } $this->add("
\n"); $this->add("\n"); $this->add("\n"); $this->add("\n"); $this->add("\n"); $this->add("\n"); $this->add("
  
\n"); $this->add("\n"); $this->add("
\n"); $this->add("
\n"); } static function ResetSession() { if (isset($_SESSION['login_mode'])) { $sPreviousLoginMode = $_SESSION['login_mode']; } else { $sPreviousLoginMode = ''; } // Unset all of the session variables. $_SESSION = array(); // If it's desired to kill the session, also delete the session cookie. // Note: This will destroy the session, and not just the session data! if (isset($_COOKIE[session_name()])) { setcookie(session_name(), '', time()-3600, '/'); } // Finally, destroy the session. session_destroy(); } static function SecureConnectionRequired() { return MetaModel::GetConfig()->GetSecureConnectionRequired(); } static function IsConnectionSecure() { $bSecured = false; if (isset($_SERVER['HTTPS']) && ($_SERVER['HTTPS']!='off')) { $bSecured = true; } return $bSecured; } protected static function Login() { if (self::SecureConnectionRequired() && !self::IsConnectionSecure()) { // Non secured URL... request for a secure connection throw new Exception('Secure connection required!'); } $aAllowedLoginTypes = MetaModel::GetConfig()->GetAllowedLoginTypes(); if (isset($_SESSION['auth_user'])) { //echo "User: ".$_SESSION['auth_user']."\n"; // Already authentified UserRights::Login($_SESSION['auth_user']); // Login & set the user's language return true; } else { $index = 0; $sLoginMode = ''; $sAuthentication = 'internal'; while(($sLoginMode == '') && ($index < count($aAllowedLoginTypes))) { $sLoginType = $aAllowedLoginTypes[$index]; switch($sLoginType) { case 'cas': utils::InitCASClient(); // check CAS authentication if (phpCAS::isAuthenticated()) { // Check is a membership is required $sCASMemberships = MetaModel::GetConfig()->Get('cas_memberof'); $bFound = false; if (!empty($sCASMemberships)) { if (phpCAS::hasAttribute('memberOf')) { // A list of groups is specified, the user must a be member of (at least) one of them to pass $aCASMemberships = array(); $aTmp = explode(';', $sCASMemberships); setlocale(LC_ALL, "en_US.utf8"); // !!! WARNING: this is needed to have the iconv //TRANSLIT working fine below !!! foreach($aTmp as $sGroupName) { $aCASMemberships[] = trim(iconv('UTF-8', 'ASCII//TRANSLIT', $sGroupName)); // Just in case remove accents and spaces... } $aMemberOf = phpCAS::getAttribute('memberOf'); if (!is_array($aMemberOf)) $aMemberOf = array($aMemberOf); // Just one entry, turn it into an array $aFilteredGroupNames = array(); foreach($aMemberOf as $sGroupName) { phpCAS::log("Info: user if a member of the group: ".$sGroupName); $sGroupName = trim(iconv('UTF-8', 'ASCII//TRANSLIT', $sGroupName)); // Remove accents and spaces as well $aFilteredGroupNames[] = $sGroupName; $bIsMember = false; foreach($aCASMemberships as $sCASPattern) { if (self::IsPattern($sCASPattern)) { if (preg_match($sCASPattern, $sGroupName)) { $bIsMember = true; break; } } else if ($sPattern == $sGroupName) { $bIsMember = true; break; } } if ($bIsMember) { $bCASUserSynchro = MetaModel::GetConfig()->Get('cas_user_synchro'); if ($bCASUserSynchro) { // If needed create a new user for this email/profile phpCAS::log('Info: cas_user_synchro is ON'); self::CreateCASUser(phpCAS::getUser(), $aMemberOf); } else { phpCAS::log('Info: cas_user_synchro is OFF'); } $bFound = true; break; } } if(!$bFound) { phpCAS::log("User ".phpCAS::getUser().", none of his/her groups (".implode('; ', $aFilteredGroupNames).") match any of the required groups: ".implode('; ', $aCASMemberships)); } } else { // Too bad, the user is not part of any of the group => not allowed phpCAS::log("No 'memberOf' attribute found for user ".phpCAS::getUser().". Are you using the SAML protocol (S1) ?"); } } else { // No membership required, anybody will pass $bFound = true; } if ($bFound) { $sAuthUser = phpCAS::getUser(); $sAuthPwd = ''; $sLoginMode = 'cas'; $sAuthentication = 'external'; } else { // The user is not part of the allowed groups, => log out $sUrl = utils::GetAbsoluteUrlAppRoot().'pages/UI.php'; $sCASLogoutUrl = MetaModel::GetConfig()->Get('cas_logout_redirect_service'); if (empty($sCASLogoutUrl)) { $sCASLogoutUrl = $sUrl; } phpCAS::logoutWithRedirectService($sCASLogoutUrl); // Redirects to the CAS logout page } } break; case 'form': // iTop standard mode: form based authentication $sAuthUser = utils::ReadPostedParam('auth_user', '', 'raw_data'); $sAuthPwd = utils::ReadPostedParam('auth_pwd', '', 'raw_data'); if ($sAuthUser != '') { $sLoginMode = 'form'; } break; case 'basic': // Standard PHP authentication method, works with Apache... // Case 1) Apache running in CGI mode + rewrite rules in .htaccess if (isset($_SERVER['HTTP_AUTHORIZATION']) && !empty($_SERVER['HTTP_AUTHORIZATION'])) { list($sAuthUser, $sAuthPwd) = explode(':' , base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6))); $sLoginMode = 'basic'; } else if (isset($_SERVER['PHP_AUTH_USER'])) { $sAuthUser = $_SERVER['PHP_AUTH_USER']; $sAuthPwd = $_SERVER['PHP_AUTH_PW']; $sLoginMode = 'basic'; } break; case 'external': // Web server supplied authentication $bExternalAuth = false; $sExtAuthVar = MetaModel::GetConfig()->GetExternalAuthenticationVariable(); // In which variable is the info passed ? eval('$sAuthUser = isset('.$sExtAuthVar.') ? '.$sExtAuthVar.' : false;'); // Retrieve the value if ($sAuthUser && (strlen($sAuthUser) > 0)) { $sAuthPwd = ''; // No password in this case the web server already authentified the user... $sLoginMode = 'external'; $sAuthentication = 'external'; } break; case 'url': // Credentials passed directly in the url $sAuthUser = utils::ReadParam('auth_user', '', false, 'raw_data'); if ($sAuthUser != '') { $sAuthPwd = utils::ReadParam('auth_pwd', '', false, 'raw_data'); $sLoginMode = 'url'; } break; } $index++; } //echo "\nsLoginMode: $sLoginMode (user: $sAuthUser / pwd: $sAuthPwd\n)"; if ($sLoginMode == '') { // First connection $sDesiredLoginMode = utils::ReadParam('login_mode'); if (in_array($sDesiredLoginMode, $aAllowedLoginTypes)) { $sLoginMode = $sDesiredLoginMode; } else { $sLoginMode = $aAllowedLoginTypes[0]; // First in the list... } $oPage = new LoginWebPage(); $oPage->DisplayLoginForm( $sLoginMode, false /* no previous failed attempt */); $oPage->output(); exit; } else { if (!UserRights::CheckCredentials($sAuthUser, $sAuthPwd, $sAuthentication)) { //echo "Check Credentials returned false for user $sAuthUser!"; self::ResetSession(); $oPage = new LoginWebPage(); $oPage->DisplayLoginForm( $sLoginMode, true /* failed attempt */); $oPage->output(); exit; } else { // User is Ok, let's save it in the session and proceed with normal login UserRights::Login($sAuthUser, $sAuthentication); // Login & set the user's language if (MetaModel::GetConfig()->Get('log_usage')) { $oLog = new EventLoginUsage(); $oLog->Set('userinfo', UserRights::GetUser()); $oLog->Set('user_id', UserRights::GetUserObject()->GetKey()); $oLog->Set('message', 'Successful login'); $oLog->DBInsertNoReload(); } $_SESSION['auth_user'] = $sAuthUser; $_SESSION['login_mode'] = $sLoginMode; } } } } /** * Check if the user is already authentified, if yes, then performs some additional validations: * - if $bMustBeAdmin is true, then the user must be an administrator, otherwise an error is displayed * - if $bIsAllowedToPortalUsers is false and the user has only access to the portal, then the user is redirected to the portal * @param bool $bMustBeAdmin Whether or not the user must be an admin to access the current page * @param bool $bIsAllowedToPortalUsers Whether or not the current page is considered as part of the portal */ static function DoLogin($bMustBeAdmin = false, $bIsAllowedToPortalUsers = false) { $sMessage = ''; // In case we need to return a message to the calling web page $operation = utils::ReadParam('loginop', ''); session_name(MetaModel::GetConfig()->Get('session_name')); session_start(); if ($operation == 'logoff') { if (isset($_SESSION['login_mode'])) { $sLoginMode = $_SESSION['login_mode']; } else { $aAllowedLoginTypes = MetaModel::GetConfig()->GetAllowedLoginTypes(); if (count($aAllowedLoginTypes) > 0) { $sLoginMode = $aAllowedLoginTypes[0]; } else { $sLoginMode = 'form'; } } self::ResetSession(); $oPage = new LoginWebPage(); $oPage->DisplayLoginForm( $sLoginMode, false /* not a failed attempt */); $oPage->output(); exit; } else if ($operation == 'change_pwd') { $sAuthUser = $_SESSION['auth_user']; UserRights::Login($sAuthUser); // Set the user's language $oPage = new LoginWebPage(); $oPage->DisplayChangePwdForm(); $oPage->output(); exit; } if ($operation == 'do_change_pwd') { $sAuthUser = $_SESSION['auth_user']; UserRights::Login($sAuthUser); // Set the user's language $sOldPwd = utils::ReadPostedParam('old_pwd', 'raw_data'); $sNewPwd = utils::ReadPostedParam('new_pwd', 'raw_data'); if (UserRights::CanChangePassword() && ((!UserRights::CheckCredentials($sAuthUser, $sOldPwd)) || (!UserRights::ChangePassword($sOldPwd, $sNewPwd)))) { $oPage = new LoginWebPage(); $oPage->DisplayChangePwdForm(true); // old pwd was wrong $oPage->output(); } $sMessage = Dict::S('UI:Login:PasswordChanged'); } self::Login(); if ($bMustBeAdmin && !UserRights::IsAdministrator()) { require_once(APPROOT.'/setup/setuppage.class.inc.php'); $oP = new SetupWebPage(Dict::S('UI:PageTitle:FatalError')); $oP->add("

".Dict::S('UI:Login:Error:AccessAdmin')."

\n"); $oP->p("".Dict::S('UI:LogOffMenu').""); $oP->output(); exit; } elseif ( (!$bIsAllowedToPortalUsers) && (UserRights::IsPortalUser())) { // No rights to be here, redirect to the portal header('Location: '.utils::GetAbsoluteUrlAppRoot().'portal/index.php'); } return $sMessage; } protected static function CreateCASUser($sEmail, $aGroups) { if (!MetaModel::IsValidClass('URP_Profiles')) { phpCAS::log("URP_Profiles is not a valid class. Automatic creation of Users is not supported in this context, sorry."); return; } // read all the existing profiles $oProfilesSearch = new DBObjectSearch('URP_Profiles'); $oProfilesSet = new DBObjectSet($oProfilesSearch); $aAllProfiles = array(); while($oProfile = $oProfilesSet->Fetch()) { $aAllProfiles[strtolower($oProfile->GetName())] = $oProfile->GetKey(); } // Translate the CAS/LDAP group names into iTop profile names $aProfiles = array(); $sPattern = MetaModel::GetConfig()->Get('cas_profile_pattern'); foreach($aGroups as $sGroupName) { if (preg_match($sPattern, $sGroupName, $aMatches)) { if (array_key_exists(strtolower($aMatches[1]), $aAllProfiles)) { $aProfiles[] = $aAllProfiles[strtolower($aMatches[1])]; } else { phpCAS::log("Warning: {$aMatches[1]} is not a valid iTop profile (extracted from group name: '$sGroupName'). Ignored."); } } } if (count($aProfiles) == 0) { phpCAS::log("Error: no group name matches the pattern: '$sPattern'. The user '$sEmail' has no profiles in iTop, and therefore cannot be created."); return; } $oUser = MetaModel::GetObjectByName('UserExternal', $sEmail, false); if ($oUser == null) { // Create the user, link it to a contact phpCAS::log("Info: the user '$sEmail' does not exist. A new UserExternal will be created."); $oSearch = new DBObjectSearch('Person'); $oSearch->AddCondition('email', $sEmail); $oSet = new DBObjectSet($oSearch); $iContactId = 0; switch($oSet->Count()) { case 0: phpCAS::log("Error: found no contact with the email: '$sEmail'. Cannot create the user in iTop."); return; case 1: $oContact = $oSet->Fetch(); $iContactId = $oContact->GetKey(); phpCAS::log("Info: Found 1 contact '".$oContact->GetName()."' (id=$iContactId) corresponding to the email '$sEmail'."); break; default: phpCAS::log("Error: ".$oSet->Count()." contacts have the same email: '$sEmail'. Cannot create a user for this email."); return; } $oUser = new UserExternal(); $oUser->Set('login', $sEmail); $oUser->Set('contactid', $iContactId); } else { phpCAS::log("Info: the user '$sEmail' already exists (id=".$oUser->GetKey().")."); } // Now synchronize the profiles $oProfilesSet = DBObjectSet::FromScratch('URP_UserProfile'); foreach($aProfiles as $iProfileId) { $oLink = new URP_UserProfile(); $oLink->Set('profileid', $iProfileId); $oLink->Set('reason', 'CAS/LDAP Synchro'); $oProfilesSet->AddObject($oLink); } $oUser->Set('profile_list', $oProfilesSet); phpCAS::log("Info: the user $sEmail (id=".$oUser->GetKey().") now has the following profiles: '".implode("', '", $aProfiles)."'."); if ($oUser->IsNew() || $oUser->IsModified()) { $oMyChange = MetaModel::NewObject("CMDBChange"); $oMyChange->Set("date", time()); $oMyChange->Set("userinfo", 'CAS/LDAP Synchro'); $oMyChange->DBInsert(); if ($oUser->IsNew()) { $oUser->DBInsertTracked($oMyChange); } else { $oUser->DBUpdateTracked($oMyChange); } } } protected static function IsPattern($sCASPattern) { if ((substr($sCASPattern, 0, 1) == '/') && (substr($sCASPattern, -1) == '/')) { // the string is enclosed by slashes, let's assume it's a pattern return true; } else { return false; } } } // End of class ?>