diff --git a/application/loginbasic.class.inc.php b/application/loginbasic.class.inc.php index fedd98062..0389ca47d 100644 --- a/application/loginbasic.class.inc.php +++ b/application/loginbasic.class.inc.php @@ -80,6 +80,11 @@ class LoginBasic extends AbstractLoginFSMExtension { if (Session::Get('login_mode') == 'basic') { + $iOnExit = LoginWebPage::getIOnExit(); + if ($iOnExit === LoginWebPage::EXIT_RETURN) + { + return LoginWebPage::LOGIN_FSM_RETURN; // Error, exit FSM + } LoginWebPage::HTTP401Error(); } return LoginWebPage::LOGIN_FSM_CONTINUE; diff --git a/application/logindefault.class.inc.php b/application/logindefault.class.inc.php index 595bf8783..26307cce1 100644 --- a/application/logindefault.class.inc.php +++ b/application/logindefault.class.inc.php @@ -79,7 +79,7 @@ class LoginDefaultAfter extends AbstractLoginFSMExtension implements iLogoutExte { self::ResetLoginSession(); $iOnExit = LoginWebPage::getIOnExit(); - if ($iOnExit == LoginWebPage::EXIT_RETURN) + if ($iOnExit === LoginWebPage::EXIT_RETURN) { return LoginWebPage::LOGIN_FSM_RETURN; // Error, exit FSM } @@ -95,6 +95,12 @@ class LoginDefaultAfter extends AbstractLoginFSMExtension implements iLogoutExte { if (!Session::IsSet('login_mode')) { + // N°6358 - if EXIT_RETURN was asked, send an error + if (LoginWebPage::getIOnExit() === LoginWebPage::EXIT_RETURN) { + $iErrorCode = LoginWebPage::EXIT_CODE_WRONGCREDENTIALS; + return LoginWebPage::LOGIN_FSM_ERROR; + } + // If no plugin validated the user, exit self::ResetLoginSession(); exit(); diff --git a/application/loginexternal.class.inc.php b/application/loginexternal.class.inc.php index da7973c23..ecb0a5fd5 100644 --- a/application/loginexternal.class.inc.php +++ b/application/loginexternal.class.inc.php @@ -73,6 +73,11 @@ class LoginExternal extends AbstractLoginFSMExtension { if (Session::Get('login_mode') == 'external') { + $iOnExit = LoginWebPage::getIOnExit(); + if ($iOnExit === LoginWebPage::EXIT_RETURN) + { + return LoginWebPage::LOGIN_FSM_RETURN; // Error, exit FSM + } LoginWebPage::HTTP401Error(); } return LoginWebPage::LOGIN_FSM_CONTINUE; diff --git a/application/loginform.class.inc.php b/application/loginform.class.inc.php index 3972e49cc..683456a41 100644 --- a/application/loginform.class.inc.php +++ b/application/loginform.class.inc.php @@ -44,6 +44,10 @@ class LoginForm extends AbstractLoginFSMExtension implements iLoginUIExtension exit; } + if (LoginWebPage::getIOnExit() === LoginWebPage::EXIT_RETURN) { + return LoginWebPage::LOGIN_FSM_CONTINUE; + } + // No credentials yet, display the form $oPage = LoginWebPage::NewLoginWebPage(); $oPage->DisplayLoginForm($this->bForceFormOnError); diff --git a/datamodels/2.x/authent-cas/src/CASLoginExtension.php b/datamodels/2.x/authent-cas/src/CASLoginExtension.php index 1617642c0..acb6ab60b 100644 --- a/datamodels/2.x/authent-cas/src/CASLoginExtension.php +++ b/datamodels/2.x/authent-cas/src/CASLoginExtension.php @@ -49,6 +49,11 @@ class CASLoginExtension extends AbstractLoginFSMExtension implements iLogoutExte protected function OnReadCredentials(&$iErrorCode) { + if (LoginWebPage::getIOnExit() === LoginWebPage::EXIT_RETURN) { + // Not allowed if not already connected + return LoginWebPage::LOGIN_FSM_CONTINUE; + } + if (empty(Session::Get('login_mode')) || Session::Get('login_mode') == static::LOGIN_MODE) { static::InitCASClient(); @@ -114,6 +119,10 @@ class CASLoginExtension extends AbstractLoginFSMExtension implements iLogoutExte if (Session::Get('login_mode') == static::LOGIN_MODE) { Session::Unset('phpCAS'); + if (LoginWebPage::getIOnExit() === LoginWebPage::EXIT_RETURN) { + // don't display the login page + return LoginWebPage::LOGIN_FSM_CONTINUE; + } if ($iErrorCode != LoginWebPage::EXIT_CODE_MISSINGLOGIN) { $oLoginWebPage = new LoginWebPage(); diff --git a/pages/ajax.searchform.php b/pages/ajax.searchform.php index 44c51c6cc..5ba9f5701 100644 --- a/pages/ajax.searchform.php +++ b/pages/ajax.searchform.php @@ -18,10 +18,7 @@ try $oKPI->ComputeAndReport('Data model loaded'); $oKPI = new ExecutionKPI(); - if (LoginWebPage::EXIT_CODE_OK != LoginWebPage::DoLoginEx('backoffice', false, LoginWebPage::EXIT_RETURN)) - { - throw new SecurityException('You must be logged in'); - } + LoginWebPage::DoLogin(); $sParams = utils::ReadParam('params', '', false, 'raw_data'); if (!$sParams) diff --git a/pages/logoff.php b/pages/logoff.php index 6fc84702d..3acd81da2 100644 --- a/pages/logoff.php +++ b/pages/logoff.php @@ -96,5 +96,11 @@ if ($bLoginDebug) IssueLog::Info("--> Display logout page"); } +LoginWebPage::ResetSession(true); +if ($bLoginDebug) { + $sSessionLog = session_id().' '.utils::GetSessionLog(); + IssueLog::Info("SESSION: $sSessionLog"); +} + $oPage = LoginWebPage::NewLoginWebPage(); $oPage->DisplayLogoutPage($bPortal); diff --git a/synchro/synchro_exec.php b/synchro/synchro_exec.php index e23f176c3..276d4ad30 100644 --- a/synchro/synchro_exec.php +++ b/synchro/synchro_exec.php @@ -101,24 +101,49 @@ if (utils::IsModeCLI()) exit -1; } } -else -{ +else { require_once(APPROOT.'/application/loginwebpage.class.inc.php'); - //N°6022 - Make synchro scripts work by http via token authentication with SYNCHRO scopes $oCtx = new ContextTag(ContextTag::TAG_SYNCHRO); - LoginWebPage::DoLogin(); // Check user rights and prompt if needed + LoginWebPage::ResetSession(true); + $iRet = LoginWebPage::DoLogin(false, false, LoginWebPage::EXIT_RETURN); + if ($iRet !== LoginWebPage::EXIT_CODE_OK) { + switch ($iRet) { + case LoginWebPage::EXIT_CODE_MISSINGLOGIN: + $oP->p("Missing parameter 'auth_user'"); + break; + + case LoginWebPage::EXIT_CODE_MISSINGPASSWORD: + $oP->p("Missing parameter 'auth_pwd'"); + break; + + case LoginWebPage::EXIT_CODE_WRONGCREDENTIALS: + $oP->p('Invalid login'); + break; + + case LoginWebPage::EXIT_CODE_PORTALUSERNOTAUTHORIZED: + $oP->p('Portal user is not allowed'); + break; + + case LoginWebPage::EXIT_CODE_NOTAUTHORIZED: + $oP->p('This user is not authorized to use the web services. (The profile REST Services User is required to access the REST web services)'); + break; + + default: + $oP->p("Unknown authentication error (retCode=$iRet)"); + } + $oP->output(); + exit - 1; + } + + $bSimulate = (utils::ReadParam('simulate', '0', true) == '1'); + $sDataSourcesList = ReadMandatoryParam($oP, 'data_sources', 'raw_data'); // May contain commas + + if ($sDataSourcesList == null) { + UsageAndExit($oP); + } } -$bSimulate = (utils::ReadParam('simulate', '0', true) == '1'); -$sDataSourcesList = ReadMandatoryParam($oP, 'data_sources', 'raw_data'); // May contain commas - -if ($sDataSourcesList == null) -{ - UsageAndExit($oP); -} - - foreach(explode(',', $sDataSourcesList) as $iSDS) { $oSynchroDataSource = MetaModel::GetObject('SynchroDataSource', $iSDS, false); @@ -178,4 +203,3 @@ foreach(explode(',', $sDataSourcesList) as $iSDS) } $oP->output(); -?> diff --git a/synchro/synchro_import.php b/synchro/synchro_import.php index 6f54abf9c..e7588587d 100644 --- a/synchro/synchro_import.php +++ b/synchro/synchro_import.php @@ -277,10 +277,38 @@ if (utils::IsModeCLI()) else { require_once APPROOT.'/application/loginwebpage.class.inc.php'; - //N°6022 - Make synchro scripts work by http via token authentication with SYNCHRO scopes $oCtx = new ContextTag(ContextTag::TAG_SYNCHRO); - LoginWebPage::DoLogin(); // Check user rights and prompt if needed + LoginWebPage::ResetSession(true); + $iRet = LoginWebPage::DoLogin(false, false, LoginWebPage::EXIT_RETURN); + if ($iRet !== LoginWebPage::EXIT_CODE_OK) { + switch ($iRet) { + case LoginWebPage::EXIT_CODE_MISSINGLOGIN: + $oP->p("Missing parameter 'auth_user'"); + break; + + case LoginWebPage::EXIT_CODE_MISSINGPASSWORD: + $oP->p("Missing parameter 'auth_pwd'"); + break; + + case LoginWebPage::EXIT_CODE_WRONGCREDENTIALS: + $oP->p('Invalid login'); + break; + + case LoginWebPage::EXIT_CODE_PORTALUSERNOTAUTHORIZED: + $oP->p('Portal user is not allowed'); + break; + + case LoginWebPage::EXIT_CODE_NOTAUTHORIZED: + $oP->p('This user is not authorized to use the web services. (The profile REST Services User is required to access the REST web services)'); + break; + + default: + $oP->p("Unknown authentication error (retCode=$iRet)"); + } + $oP->output(); + exit -1; + } $sCSVData = utils::ReadPostedParam('csvdata', '', 'raw_data'); } diff --git a/tests/php-unit-tests/unitary-tests/webservices/CliResetSessionTest.php b/tests/php-unit-tests/unitary-tests/webservices/CliResetSessionTest.php new file mode 100644 index 000000000..cd03f3cfd --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/webservices/CliResetSessionTest.php @@ -0,0 +1,257 @@ +sConfigTmpBackupFile = tempnam(sys_get_temp_dir(), "config_"); + MetaModel::GetConfig()->WriteToFile($this->sConfigTmpBackupFile); + + $this->sLogin = "rest-user-".date('dmYHis'); + $this->CreateTestOrganization(); + + $this->sCookieFile = tempnam(sys_get_temp_dir(), 'jsondata_'); + + $this->sUrl = \MetaModel::GetConfig()->Get('app_root_url'); + + $oRestProfile = \MetaModel::GetObjectFromOQL("SELECT URP_Profiles WHERE name = :name", array('name' => 'REST Services User'), true); + $oAdminProfile = \MetaModel::GetObjectFromOQL("SELECT URP_Profiles WHERE name = :name", array('name' => 'Administrator'), true); + + if (is_object($oRestProfile) && is_object($oAdminProfile)) { + $oUser = $this->CreateUser($this->sLogin, $oRestProfile->GetKey(), $this->sPassword); + $this->sUserId = $oUser->GetKey(); + $this->AddProfileToUser($oUser, $oAdminProfile->GetKey()); + } + } + + protected function tearDown(): void { + parent::tearDown(); + + if (! is_null($this->sConfigTmpBackupFile) && is_file($this->sConfigTmpBackupFile)){ + //put config back + $sConfigPath = MetaModel::GetConfig()->GetLoadedFile(); + @chmod($sConfigPath, 0770); + $oConfig = new Config($this->sConfigTmpBackupFile); + $oConfig->WriteToFile($sConfigPath); + @chmod($sConfigPath, 0440); + unlink($this->sConfigTmpBackupFile); + } + + if (!empty($this->sCookieFile)) { + unlink($this->sCookieFile); + } + } + + protected function AddLoginMode($sLoginMode){ + @chmod(MetaModel::GetConfig()->GetLoadedFile(), 0770); + $aAllowedLoginTypes = MetaModel::GetConfig()->GetAllowedLoginTypes(); + if (! in_array($sLoginMode, $aAllowedLoginTypes)){ + $aAllowedLoginTypes[] = $sLoginMode; + MetaModel::GetConfig()->SetAllowedLoginTypes($aAllowedLoginTypes); + MetaModel::GetConfig()->WriteToFile(); + } + @chmod(MetaModel::GetConfig()->GetLoadedFile(), 0440); + } + + protected function SetLoginModes($aAllowedLoginTypes){ + @chmod(MetaModel::GetConfig()->GetLoadedFile(), 0770); + MetaModel::GetConfig()->SetAllowedLoginTypes($aAllowedLoginTypes); + MetaModel::GetConfig()->WriteToFile(); + @chmod(MetaModel::GetConfig()->GetLoadedFile(), 0440); + } + + public function RestProvider(){ + return [ + 'nominal / no login_mode forced' => [ + 'sConfiguredLoginModes' => 'form|external|basic', + 'sForcedLoginMode' => null, + ], + 'nominal / form forced' => [ + 'sConfiguredLoginModes' => 'form|external|basic', + 'sForcedLoginMode' => 'form', + ], + 'nominal / external forced' => [ + 'sConfiguredLoginModes' => 'form|external|basic', + 'sForcedLoginMode' => 'external', + ], + 'nominal / basic forced' => [ + 'sConfiguredLoginModes' => 'form|external|basic', + 'sForcedLoginMode' => 'basic', + ], + 'nominal / url forced' => [ + 'sConfiguredLoginModes' => 'form|external|basic|url', + 'sForcedLoginMode' => 'url', + ], + 'nominal / cas forced' => [ + 'sConfiguredLoginModes' => 'form|external|basic|cas', + 'sForcedLoginMode' => 'cas', + ], + ]; + } + + /** + * @dataProvider RestProvider + * @param $aLoginModes + * @param $sForcedLoginMode + * + * @return void + */ + public function testRest($sConfiguredLoginModes=null, $sForcedLoginMode=null, $sExpectedFailHttpCode="200"){ + if (! is_null($sConfiguredLoginModes)){ + $this->SetLoginModes(explode('|', $sConfiguredLoginModes)); + } + + $sJsonGetContent = <<sUserId, + "output_fields": "id" +} +JSON; + $aPostFields = [ + 'version' => '1.2', + 'auth_user' => $this->sLogin, + 'auth_pwd' => $this->sPassword, + 'json_data' => $sJsonGetContent, + ]; + list($iHttpCode, $sJson) = $this->CallRestApi($aPostFields); + $this->assertEquals(200, $iHttpCode); + + $aJson = json_decode($sJson, true); + $this->assertTrue(is_array($aJson), $sJson); + $this->assertEquals("0", $aJson['code'], $sJson); + + //2nd call to REST API made with previous session cookie + //no need to pass auth_user/auth_pwd + $aPostFields = [ + 'version' => '1.2', + 'json_data' => $sJsonGetContent, + ]; + list($iHttpCode, $sJson) = $this->CallRestApi($aPostFields, $sForcedLoginMode); + $this->debug($sJson); + $this->assertEquals($sExpectedFailHttpCode, $iHttpCode); + if ($iHttpCode === "200") { + $this->assertEquals('{"code":1,"message":"Error: Invalid login"}', $sJson); + } + } + + public function OtherCliProvider(){ + return [ + 'import' => [ 'webservices/import.php' ], + 'synchro_exec' => [ 'synchro/synchro_exec.php' ], + 'synchro_import' => [ 'synchro/synchro_import.php' ], + ]; + } + + /** + * @dataProvider OtherCliProvider + */ + public function testImport($sUri){ + $sJsonGetContent = <<sUserId, + "output_fields": "id" +} +JSON; + $aPostFields = [ + 'version' => '1.2', + 'auth_user' => $this->sLogin, + 'auth_pwd' => $this->sPassword, + 'json_data' => $sJsonGetContent, + ]; + list($iHttpCode, $sOutput) = $this->CallRestApi($aPostFields); + $this->assertEquals(200, $iHttpCode); + + $aJson = json_decode($sOutput, true); + $this->assertTrue(is_array($aJson), $sOutput); + $this->assertEquals("0", $aJson['code'], $sOutput); + + //2nd call to REST API made with previous session cookie + //no need to pass auth_user/auth_pwd + $aPostFields = [ + 'version' => '1.2', + 'json_data' => $sJsonGetContent, + ]; + list($iHttpCode, $sOutput) = $this->CallRestApi($aPostFields, null, $sUri); + $this->debug($sOutput); + $this->assertEquals("200", $iHttpCode); + $this->assertContains("Invalid login", $sOutput); + } + + /** + * @param $aPostFields + * + * @return array($iHttpCode, $sBody) + */ + private function CallRestApi($aPostFields, $sForcedLoginMode=null, $sUri='webservices/rest.php'): array { + $ch = curl_init(); + + curl_setopt ($ch, CURLOPT_COOKIEJAR, $this->sCookieFile); + curl_setopt ($ch, CURLOPT_COOKIEFILE, $this->sCookieFile); + + $sUrl = "$this->sUrl/$sUri"; + if (!is_null($sForcedLoginMode)){ + $sUrl .= "?login_mode=$sForcedLoginMode"; + } + curl_setopt($ch, CURLOPT_URL, $sUrl); + curl_setopt($ch, CURLOPT_POST, 1);// set post data to true + curl_setopt($ch, CURLOPT_POSTFIELDS, $aPostFields); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_HEADER, 1); + // Force disable of certificate check as most of dev / test env have a self-signed certificate + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); + + $sResponse = curl_exec($ch); + /** $sResponse example + * "HTTP/1.1 200 OK + Date: Wed, 07 Jun 2023 05:00:40 GMT + Server: Apache/2.4.29 (Ubuntu) + Set-Cookie: itop-2e83d2e9b00e354fdc528621cac532ac=q7ldcjq0rvbn33ccr9q8u8e953; path=/ + */ + //var_dump($sResponse); + $iHeaderSize = curl_getinfo($ch,CURLINFO_HEADER_SIZE); + $sBody = substr($sResponse, $iHeaderSize); + + //$iHttpCode = intval(curl_getinfo($ch, CURLINFO_HTTP_CODE)); + if (preg_match('/HTTP.* (\d*) /', $sResponse, $aMatches)){ + $sHttpCode = $aMatches[1]; + } else { + $sHttpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + } + curl_close ($ch); + + return array($sHttpCode, $sBody); + } +} diff --git a/webservices/import.php b/webservices/import.php index b337868db..3eb79a992 100644 --- a/webservices/import.php +++ b/webservices/import.php @@ -258,7 +258,36 @@ if (utils::IsModeCLI()) else { require_once(APPROOT.'/application/loginwebpage.class.inc.php'); - LoginWebPage::DoLogin(); // Check user rights and prompt if needed + LoginWebPage::ResetSession(true); + $iRet = LoginWebPage::DoLogin(false, false, LoginWebPage::EXIT_RETURN); + if ($iRet !== LoginWebPage::EXIT_CODE_OK) { + switch ($iRet) { + case LoginWebPage::EXIT_CODE_MISSINGLOGIN: + $oP->p("Missing parameter 'auth_user'"); + break; + + case LoginWebPage::EXIT_CODE_MISSINGPASSWORD: + $oP->p("Missing parameter 'auth_pwd'"); + break; + + case LoginWebPage::EXIT_CODE_WRONGCREDENTIALS: + $oP->p('Invalid login'); + break; + + case LoginWebPage::EXIT_CODE_PORTALUSERNOTAUTHORIZED: + $oP->p('Portal user is not allowed'); + break; + + case LoginWebPage::EXIT_CODE_NOTAUTHORIZED: + $oP->p('This user is not authorized to use the web services. (The profile REST Services User is required to access the REST web services)'); + break; + + default: + $oP->p("Unknown authentication error (retCode=$iRet)"); + } + $oP->output(); + exit -1; + } $sCSVData = utils::ReadPostedParam('csvdata', '', 'raw_data'); } diff --git a/webservices/rest.php b/webservices/rest.php index 4b158ccb9..14015dc0b 100644 --- a/webservices/rest.php +++ b/webservices/rest.php @@ -95,10 +95,12 @@ try $oKPI->ComputeAndReport('Data model loaded'); - $iRet = LoginWebPage::DoLogin(false, false, LoginWebPage::EXIT_RETURN); // Starting with iTop 2.2.0 portal users are no longer allowed to access the REST/JSON API - $oKPI->ComputeAndReport('User login'); - - if ($iRet == LoginWebPage::EXIT_CODE_OK) + // N°6358 - force credentials for REST calls + LoginWebPage::ResetSession(true); + $iRet = LoginWebPage::DoLogin(false, false, LoginWebPage::EXIT_RETURN); + $oKPI->ComputeAndReport('User login'); + + if ($iRet == LoginWebPage::EXIT_CODE_OK) { // Extra validation of the profile if ((MetaModel::GetConfig()->Get('secure_rest_services') == true) && !UserRights::HasProfile('REST Services User')) @@ -109,7 +111,7 @@ try } if ($iRet != LoginWebPage::EXIT_CODE_OK) { - switch($iRet) + switch($iRet) { case LoginWebPage::EXIT_CODE_MISSINGLOGIN: throw new Exception("Missing parameter 'auth_user'", RestResult::MISSING_AUTH_USER);