From 59fa3e10a3f85ced2a817e1e4245c00b355025ff Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 22 Aug 2019 13:53:30 +0200 Subject: [PATCH] =?UTF-8?q?N=C2=B02311=20-=20User=20Provisioning=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/logindefault.class.inc.php | 2 +- application/loginwebpage.class.inc.php | 239 +++++++++++++++++- .../2.x/authent-cas/src/CASLoginExtension.php | 105 +++----- dictionaries/en.dictionary.itop.ui.php | 3 + dictionaries/fr.dictionary.itop.ui.php | 3 + 5 files changed, 280 insertions(+), 72 deletions(-) diff --git a/application/logindefault.class.inc.php b/application/logindefault.class.inc.php index 001e27db4..3921b7a6f 100644 --- a/application/logindefault.class.inc.php +++ b/application/logindefault.class.inc.php @@ -85,7 +85,7 @@ class LoginDefaultAfter extends AbstractLoginFSMExtension implements iLogoutExte return LoginWebPage::LOGIN_FSM_RETURN_CONTINUE; } - protected function OnCheckCredentials(&$iErrorCode) + protected function OnCredentialsOk(&$iErrorCode) { if (!isset($_SESSION['login_mode'])) { diff --git a/application/loginwebpage.class.inc.php b/application/loginwebpage.class.inc.php index 90dbdd332..ab8faedb3 100644 --- a/application/loginwebpage.class.inc.php +++ b/application/loginwebpage.class.inc.php @@ -361,7 +361,7 @@ EOF { $sAuthUser = utils::ReadParam('auth_user', '', false, 'raw_data'); $sToken = utils::ReadParam('token', '', false, 'raw_data'); - $sNewPwd = utils::ReadPostedParam('new_pwd', '', false, 'raw_data'); + $sNewPwd = utils::ReadPostedParam('new_pwd', '', 'raw_data'); UserRights::Login($sAuthUser); // Set the user's language $oUser = UserRights::GetUserObject(); @@ -679,7 +679,28 @@ EOF } /** - * Store User info in the session when connection is OK + * Login API: Check that credentials correspond to a valid user + * + * @param string $sName + * @param string $sPassword + * @param string $sAuthentication + * + * @return bool + * @api + */ + public static function CheckUser($sName, $sPassword, $sAuthentication = 'external') + { + $oUser = self::FindUser($sName, true, ucfirst(strtolower($sAuthentication))); + if (is_null($oUser)) + { + return false; + } + + return $oUser->CheckCredentials($sPassword); + } + + /** + * Login API: Store User info in the session when connection is OK * * @param $sAuthUser * @param $sAuthentication @@ -692,6 +713,7 @@ EOF * @throws CoreWarning * @throws MySQLException * @throws OQLException + * @api */ public static function OnLoginSuccess($sAuthUser, $sAuthentication, $sLoginMode) { @@ -714,6 +736,14 @@ EOF UserRights::_InitSessionCache(); } + /** + * Login API: Check that an already logger User is still valid + * + * @param int $iErrorCode + * + * @return int LOGIN_FSM_RETURN_OK or LOGIN_FSM_RETURN_ERROR + * @api + */ public static function CheckLoggedUser(&$iErrorCode) { if (isset($_SESSION['auth_user'])) @@ -744,6 +774,211 @@ EOF exit; } + /** + * Provisioning API: Find a User + * + * @api + * + * @param bool $bMustBeValid + * @param string $sType + * + * @param string $sLogin + * + * @return \User|null + */ + public static function FindUser($sLogin, $bMustBeValid = true, $sType = 'External') + { + try + { + $aArgs = array('login' => $sLogin); + $sUserClass = "User$sType"; + $oSearch = DBObjectSearch::FromOQL("SELECT $sUserClass WHERE login = :login"); + if ($bMustBeValid) + { + $oSearch->AddCondition('status', 'enabled'); + } + $oSet = new DBObjectSet($oSearch, array(), $aArgs); + if ($oSet->CountExceeds(0)) + { + /** @var User $oUser */ + $oUser = $oSet->Fetch(); + + return $oUser; + } + } + catch (Exception $e) + { + IssueLog::Error($e->getMessage()); + } + return null; + } + + /** + * Provisioning API: Find a Person + * + * @param string $sEmail + * + * @return \DBObject + * @throws \CoreException + * @throws \CoreUnexpectedValue + * @throws \MissingQueryArgument + * @throws \MySQLException + * @throws \MySQLHasGoneAwayException + * @throws \Exception + * @api + */ + public static function FindPerson($sEmail) + { + $oSearch = new DBObjectSearch('Person'); + $oSearch->AddCondition('email', $sEmail); + $oSet = new DBObjectSet($oSearch); + if ($oSet->CountExceeds(1)) + { + throw new Exception(Dict::S('UI:Login:Error:MultipleContactsHaveSameEmail')); + } + return $oSet->Fetch(); + } + + /** + * Provisioning API: Create a person + * + * @param string $sFirstName + * @param string $sLastName + * @param string $sEmail + * @param string $sOrganization + * @param array $aAdditionalParams + * + * @return \Person + * @throws \ArchivedObjectException + * @throws \CoreCannotSaveObjectException + * @throws \CoreException + * @throws \CoreUnexpectedValue + * @throws \CoreWarning + * @throws \MySQLException + * @throws \OQLException + * @api + */ + public static function ProvisionPerson($sFirstName, $sLastName, $sEmail, $sOrganization, $aAdditionalParams = array()) + { + /** @var Person $oPerson */ + $oPerson = MetaModel::NewObject('Person'); + $oPerson->Set('first_name', $sFirstName); + $oPerson->Set('name', $sLastName); + $oPerson->Set('email', $sEmail); + $oOrg = MetaModel::GetObjectByName('Organization', $sOrganization, false); + if (is_null($oOrg)) + { + throw new Exception(Dict::S('UI:Login:Error:WrongOrganizationName')); + } + $oPerson->Set('org_id', $oOrg->GetKey()); + foreach ($aAdditionalParams as $sAttCode => $sValue) + { + $oPerson->Set($sAttCode, $sValue); + } + /** @var CMDBChange $oMyChange */ + $oMyChange = MetaModel::NewObject('CMDBChange'); + $oMyChange->Set("date", time()); + $sOrigin = 'External User provisioning'; + if (isset($_SESSION['login_mode'])) + { + $sOrigin .= " ({$_SESSION['login_mode']})"; + } + $oMyChange->Set('userinfo', $sOrigin); + $oMyChange->DBInsert(); + $oPerson->DBInsertTracked($oMyChange); + return $oPerson; + } + + /** + * Provisioning API: Create or update a User + * + * @param string $sLogin + * @param Person $oPerson + * @param array $aRequestedProfiles + * + * @return \cmdbAbstractObject|\UserExternal + * @throws \CoreException + * @throws \CoreUnexpectedValue + * @throws \Exception + * @api + */ + public static function ProvisionUser($sLogin, $oPerson, $aRequestedProfiles) + { + if (!MetaModel::IsValidClass('URP_Profiles')) + { + IssueLog::Error("URP_Profiles is not a valid class. Automatic creation of Users is not supported in this context, sorry."); + return null; + } + + /** @var UserExternal $oUser */ + $oUser = MetaModel::GetObjectByName('UserExternal', $sLogin, false); + if (is_null($oUser)) + { + $oUser = MetaModel::NewObject('UserExternal'); + $oUser->Set('login', $sLogin); + $oUser->Set('contactid', $oPerson->GetKey()); + $oUser->Set('language', MetaModel::GetConfig()->GetDefaultLanguage()); + } + + // 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(); + } + + $aProfiles = array(); + foreach ($aRequestedProfiles as $sRequestedProfile) + { + $sRequestedProfile = strtolower($sRequestedProfile); + if (isset($aAllProfiles[$sRequestedProfile])) + { + $aProfiles[] = $aAllProfiles[$sRequestedProfile]; + } + } + + if (empty($aProfiles)) + { + throw new Exception(Dict::S('UI:Login:Error:NoValidProfiles')); + } + + // Now synchronize the profiles + $oProfilesSet = DBObjectSet::FromScratch('URP_UserProfile'); + $sOrigin = 'External User provisioning'; + if (isset($_SESSION['login_mode'])) + { + $sOrigin .= " ({$_SESSION['login_mode']})"; + } + foreach($aProfiles as $iProfileId) + { + $oLink = new URP_UserProfile(); + $oLink->Set('profileid', $iProfileId); + $oLink->Set('reason', $sOrigin); + $oProfilesSet->AddObject($oLink); + } + $oUser->Set('profile_list', $oProfilesSet); + if ($oUser->IsModified()) + { + /** @var \CMDBChange $oMyChange */ + $oMyChange = MetaModel::NewObject("CMDBChange"); + $oMyChange->Set("date", time()); + $oMyChange->Set('userinfo', $sOrigin); + $oMyChange->DBInsert(); + if ($oUser->IsNew()) + { + $oUser->DBInsertTracked($oMyChange); + } + else + { + $oUser->DBUpdateTracked($oMyChange); + } + } + + return $oUser; + } + /** * Overridable: depending on the user, head toward a dedicated portal * @param string|null $sRequestedPortalId diff --git a/datamodels/2.x/authent-cas/src/CASLoginExtension.php b/datamodels/2.x/authent-cas/src/CASLoginExtension.php index 3eb594ba8..5b804cb48 100644 --- a/datamodels/2.x/authent-cas/src/CASLoginExtension.php +++ b/datamodels/2.x/authent-cas/src/CASLoginExtension.php @@ -18,7 +18,6 @@ use phpCAS; use URP_UserProfile; use User; use UserExternal; -use UserRights; use utils; /** @@ -92,7 +91,7 @@ class CASLoginExtension extends AbstractLoginFSMExtension implements iLogoutExte if ($_SESSION['login_mode'] == 'cas') { $sAuthUser = $_SESSION['auth_user']; - if (!UserRights::CheckCredentials($sAuthUser, '', $_SESSION['login_mode'], 'external')) + if (!LoginWebPage::CheckUser($sAuthUser, '', 'external')) { $iErrorCode = LoginWebPage::EXIT_CODE_NOTAUTHORIZED; return LoginWebPage::LOGIN_FSM_RETURN_ERROR; @@ -184,16 +183,22 @@ class CASLoginExtension extends AbstractLoginFSMExtension implements iLogoutExte private function DoUserProvisioning($sLogin) { - $aArgs = array('login' => $sLogin); - $oSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT UserExternal WHERE login = :login"), array(), $aArgs); - if ($oSet->CountExceeds(0)) + $bCASUserSynchro = Config::Get('cas_user_synchro'); + if (!$bCASUserSynchro) { - /** @var User $oUser */ - $oUser = $oSet->Fetch(); - CASUserProvisioning::UpdateUser($oUser); return; } - CASUserProvisioning::CreateUser($sLogin, '', 'cas', 'external'); + + $oUser = LoginWebPage::FindUser($sLogin, false); + if ($oUser) + { + if ($oUser->Get('status') == 'enabled') + { + CASUserProvisioning::UpdateUser($oUser); + } + return; + } + CASUserProvisioning::CreateUser($sLogin, '', 'external'); } } @@ -207,11 +212,6 @@ class CASUserProvisioning * Called when no user is found in iTop for the corresponding 'name'. This method * can create/synchronize the User in iTop with an external source (such as AD/LDAP) on the fly * - * @param string $sName The CAS authenticated user name - * @param string $sPassword Ignored - * @param string $sLoginMode The login mode used (cas|form|basic|url) - * @param string $sAuthentication The authentication method used - * * @return bool true if the user is a valid one, false otherwise * @throws \ArchivedObjectException * @throws \CoreCannotSaveObjectException @@ -223,10 +223,9 @@ class CASUserProvisioning * @throws \MySQLHasGoneAwayException * @throws \OQLException */ - public static function CreateUser($sName, $sPassword, $sLoginMode, $sAuthentication) + public static function CreateUser() { $bOk = true; - if ($sLoginMode != 'cas') return false; // Must be authenticated via CAS $sCASMemberships = Config::Get('cas_memberof'); $bFound = false; @@ -271,25 +270,15 @@ class CASUserProvisioning } if ($bIsMember) { - $bCASUserSynchro = Config::Get('cas_user_synchro'); - if ($bCASUserSynchro) + // If needed create a new user for this email/profile + $bOk = self::CreateCASUser(phpCAS::getUser(), $aMemberOf); + if($bOk) { - // If needed create a new user for this email/profile - phpCAS::log('Info: cas_user_synchro is ON'); - $bOk = self::CreateCASUser(phpCAS::getUser(), $aMemberOf); - if($bOk) - { - $bFound = true; - } - else - { - phpCAS::log("User ".phpCAS::getUser()." cannot be created in iTop. Logging off..."); - } + $bFound = true; } else { - phpCAS::log('Info: cas_user_synchro is OFF'); - $bFound = true; + phpCAS::log("User ".phpCAS::getUser()." cannot be created in iTop. Logging off..."); } break; } @@ -315,11 +304,10 @@ class CASUserProvisioning if (!$bFound) { // The user is not part of the allowed groups, => log out - $sUrl = utils::GetAbsoluteUrlAppRoot().'pages/UI.php'; $sCASLogoutUrl = Config::Get('cas_logout_redirect_service'); if (empty($sCASLogoutUrl)) { - $sCASLogoutUrl = $sUrl; + $sCASLogoutUrl = utils::GetAbsoluteUrlAppRoot().'pages/UI.php'; } phpCAS::logoutWithRedirectService($sCASLogoutUrl); // Redirects to the CAS logout page // Will never return ! @@ -355,21 +343,17 @@ class CASUserProvisioning /** * Helper method to create a CAS based user * - * @param string $sEmail + * @param string $sLogin * @param array $aGroups * * @return bool true on success, false otherwise - * @throws \ArchivedObjectException - * @throws \CoreCannotSaveObjectException * @throws \CoreException * @throws \CoreUnexpectedValue - * @throws \CoreWarning * @throws \MissingQueryArgument * @throws \MySQLException * @throws \MySQLHasGoneAwayException - * @throws \OQLException */ - protected static function CreateCASUser($sEmail, $aGroups) + protected static function CreateCASUser($sLogin, $aGroups) { if (!MetaModel::IsValidClass('URP_Profiles')) { @@ -377,15 +361,22 @@ class CASUserProvisioning return false; } - $oUser = MetaModel::GetObjectByName('UserExternal', $sEmail, false); + $oUser = MetaModel::GetObjectByName('UserExternal', $sLogin, 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."); + if (phpCAS::hasAttribute('mail')) + { + $sEmail = phpCAS::getAttribute('mail'); + } + else + { + $sEmail = $sLogin; + } + phpCAS::log("Info: the user '$sLogin' 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: @@ -404,41 +395,17 @@ class CASUserProvisioning } $oUser = new UserExternal(); - $oUser->Set('login', $sEmail); + $oUser->Set('login', $sLogin); $oUser->Set('contactid', $iContactId); $oUser->Set('language', MetaModel::GetConfig()->GetDefaultLanguage()); } else { - phpCAS::log("Info: the user '$sEmail' already exists (id=".$oUser->GetKey().")."); + phpCAS::log("Info: the user '$sLogin' already exists (id=".$oUser->GetKey().")."); } // Now synchronize the profiles - if (!self::SetProfilesFromCAS($oUser, $aGroups)) - { - return false; - } - else - { - if ($oUser->IsNew() || $oUser->IsModified()) - { - /** @var \CMDBChange $oMyChange */ - $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); - } - } - - return true; - } + return self::SetProfilesFromCAS($oUser, $aGroups); } /** diff --git a/dictionaries/en.dictionary.itop.ui.php b/dictionaries/en.dictionary.itop.ui.php index d49551403..10cf581fc 100644 --- a/dictionaries/en.dictionary.itop.ui.php +++ b/dictionaries/en.dictionary.itop.ui.php @@ -562,6 +562,9 @@ Dict::Add('EN US', 'English', 'English', array( 'UI:Button:Login' => 'Enter iTop', 'UI:Login:Error:AccessRestricted' => 'iTop access is restricted. Please, contact an iTop administrator.', 'UI:Login:Error:AccessAdmin' => 'Access restricted to people having administrator privileges. Please, contact an iTop administrator.', + 'UI:Login:Error:WrongOrganizationName' => 'Unknown organization', + 'UI:Login:Error:MultipleContactsHaveSameEmail' => 'Multiple contacts have the same e-mail', + 'UI:Login:Error:NoValidProfiles' => 'No valid profile provided', 'UI:CSVImport:MappingSelectOne' => '-- select one --', 'UI:CSVImport:MappingNotApplicable' => '-- ignore this field --', 'UI:CSVImport:NoData' => 'Empty data set..., please provide some data!', diff --git a/dictionaries/fr.dictionary.itop.ui.php b/dictionaries/fr.dictionary.itop.ui.php index e581867b0..e82cbdb15 100644 --- a/dictionaries/fr.dictionary.itop.ui.php +++ b/dictionaries/fr.dictionary.itop.ui.php @@ -545,6 +545,9 @@ Dict::Add('FR FR', 'French', 'Français', array( 'UI:Button:Login' => 'Entrer dans iTop', 'UI:Login:Error:AccessRestricted' => 'L\'accès à iTop est soumis à autorisation. Merci de contacter votre administrateur iTop.', 'UI:Login:Error:AccessAdmin' => 'Accès resreint aux utilisateurs possédant le profil Administrateur.', + 'UI:Login:Error:WrongOrganizationName' => 'Organisation inconnue', + 'UI:Login:Error:MultipleContactsHaveSameEmail' => 'Email partagé par plusieurs contacts', + 'UI:Login:Error:NoValidProfiles' => 'Pas de profil valide', 'UI:CSVImport:MappingSelectOne' => '-- choisir une valeur --', 'UI:CSVImport:MappingNotApplicable' => '-- ignorer ce champ --', 'UI:CSVImport:NoData' => 'Aucune donnée... merci de fournir des données !',