N°2311 - User Provisioning API

This commit is contained in:
Eric
2019-08-22 13:53:30 +02:00
parent 0831a427cc
commit 59fa3e10a3
5 changed files with 280 additions and 72 deletions

View File

@@ -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']))
{

View File

@@ -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

View File

@@ -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);
}
/**

View File

@@ -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!',

View File

@@ -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 !',