Merge pull request #481

* N°4010 - Configuration file protection against server changes

* N°4010 - Configuration file editor - Fix reset button
This commit is contained in:
Eric Espié
2023-04-25 09:35:54 +02:00
committed by GitHub
parent 284117f33a
commit 5644587e5d
3 changed files with 161 additions and 134 deletions

View File

@@ -4,6 +4,7 @@
* @license http://opensource.org/licenses/AGPL-3.0
*/
use Combodo\iTop\Application\UI\Base\Component\Alert\Alert;
use Combodo\iTop\Application\UI\Base\Component\Alert\AlertUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\Button\ButtonUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\Form\Form;
@@ -15,6 +16,10 @@ use Combodo\iTop\Config\Validator\iTopConfigSyntaxValidator;
require_once(APPROOT.'application/startup.inc.php');
const CONFIG_ERROR = 0;
const CONFIG_WARNING = 1;
const CONFIG_INFO = 2;
/**
* @param $sContents
@@ -45,31 +50,51 @@ function DBPasswordInNewConfigIsOk($sSafeContent)
if ($bIsWindows && (preg_match("@'db_pwd' => '[^%!\"]+',@U", $sSafeContent) === 0)) {
return false;
}
return true;
}
function CheckAsyncTasksRetryConfig(Config $oTempConfig, iTopWebPage $oP)
{
$iWarnings = 0;
foreach(get_declared_classes() as $sPHPClass)
{
$oRefClass = new ReflectionClass($sPHPClass);
if ($oRefClass->isSubclassOf('AsyncTask') && !$oRefClass->isAbstract())
{
$aMessages = AsyncTask::CheckRetryConfig($oTempConfig, $oRefClass->getName());
$iWarnings = 0;
foreach (get_declared_classes() as $sPHPClass) {
$oRefClass = new ReflectionClass($sPHPClass);
if ($oRefClass->isSubclassOf('AsyncTask') && !$oRefClass->isAbstract()) {
$aMessages = AsyncTask::CheckRetryConfig($oTempConfig, $oRefClass->getName());
if (count($aMessages) !== 0)
{
foreach($aMessages as $sMessage)
{
$oAlert = AlertUIBlockFactory::MakeForWarning('', $sMessage);
$oP->AddUiBlock($oAlert);
$iWarnings ++;
}
}
}
}
return $iWarnings;
if (count($aMessages) !== 0) {
foreach ($aMessages as $sMessage) {
$oAlert = AlertUIBlockFactory::MakeForWarning('', $sMessage);
$oP->AddUiBlock($oAlert);
$iWarnings++;
}
}
}
}
return $iWarnings;
}
/**
* @param \Exception $e
*
* @return \Combodo\iTop\Application\UI\Base\Component\Alert\Alert
*/
function GetAlertFromException(Exception $e): Alert
{
switch ($e->getCode()) {
case CONFIG_WARNING:
$oAlert = AlertUIBlockFactory::MakeForWarning('', $e->getMessage());
break;
case CONFIG_INFO:
$oAlert = AlertUIBlockFactory::MakeForInformation('', $e->getMessage());
break;
case CONFIG_ERROR:
default:
$oAlert = AlertUIBlockFactory::MakeForDanger('', $e->getMessage());
}
return $oAlert;
}
/////////////////////////////////////////////////////////////////////
@@ -97,111 +122,112 @@ try {
$oP->AddUiBlock(TitleUIBlockFactory::MakeForPage(Dict::S('config-edit-title')));
if (MetaModel::GetConfig()->Get('demo_mode')) {
$oAlert = AlertUIBlockFactory::MakeForInformation('', "Sorry, iTop is in <b>demonstration mode</b>: the configuration file cannot be edited.");
$oP->AddUiBlock($oAlert);
} else {
if (MetaModel::GetModuleSetting('itop-config', 'config_editor', '') == 'disabled') {
$oAlert = AlertUIBlockFactory::MakeForWarning('', "iTop interactive edition of the configuration as been disabled. See <tt>'config_editor' => 'disabled'</tt> in the configuration file.");
throw new Exception(Dict::S('config-not-allowed-in-demo'), CONFIG_INFO);
}
if (MetaModel::GetModuleSetting('itop-config', 'config_editor', '') == 'disabled') {
throw new Exception(Dict::S('config-interactive-not-allowed'), CONFIG_WARNING);
}
$sConfigFile = APPROOT.'conf/'.utils::GetCurrentEnvironment().'/config-itop.php';
$iEditorTopMargin += 9;
$sConfigContent = file_get_contents($sConfigFile);
$sConfigChecksum = md5($sConfigContent);
$sConfig = str_replace("\r\n", "\n", $sConfigContent);
$sOriginalConfig = $sConfig;
if (!empty($sOperation)) {
$iEditorTopMargin += 5;
$sConfig = utils::ReadParam('new_config', '', false, 'raw_data');
}
try {
if ($sOperation == 'revert') {
throw new Exception(Dict::S('config-reverted'), CONFIG_WARNING);
}
if ($sOperation == 'save') {
$sTransactionId = utils::ReadParam('transaction_id', '', false, 'transaction_id');
if (!utils::IsTransactionValid($sTransactionId, true)) {
throw new Exception(Dict::S('config-error-transaction'), CONFIG_ERROR);
}
$sChecksum = utils::ReadParam('checksum');
if ($sChecksum !== $sConfigChecksum) {
throw new Exception(Dict::S('config-error-file-changed'), CONFIG_ERROR);
}
if ($sConfig === $sOriginalConfig) {
throw new Exception(Dict::S('config-no-change'), CONFIG_INFO);
}
TestConfig($sConfig, $oP); // throws exceptions
@chmod($sConfigFile, 0770); // Allow overwriting the file
$sTmpFile = tempnam(SetupUtils::GetTmpDir(), 'itop-cfg-');
// Don't write the file as-is since it would allow to inject any kind of PHP code.
// Instead, write the interpreted version of the file
// Note:
// The actual raw PHP code will anyhow be interpreted exactly twice: once in TestConfig() above
// and a second time during the load of the Config object below.
// If you are really concerned about an iTop administrator crafting some malicious
// PHP code inside the config file, then turn off the interactive configuration
// editor by adding the configuration parameter:
// 'itop-config' => array(
// 'config_editor' => 'disabled',
// )
file_put_contents($sTmpFile, $sConfig);
$oTempConfig = new Config($sTmpFile, true);
$oTempConfig->WriteToFile($sConfigFile);
@unlink($sTmpFile);
@chmod($sConfigFile, 0440); // Read-only
if (DBPasswordInNewConfigIsOk($sConfig)) {
$oAlert = AlertUIBlockFactory::MakeForSuccess('', Dict::S('config-saved'));
} else {
$oAlert = AlertUIBlockFactory::MakeForInformation('', Dict::S('config-saved-warning-db-password'));
}
$oP->AddUiBlock($oAlert);
} else {
$sConfigFile = APPROOT.'conf/'.utils::GetCurrentEnvironment().'/config-itop.php';
$iEditorTopMargin += 9;
$sConfig = str_replace("\r\n", "\n", file_get_contents($sConfigFile));
$iWarnings = CheckAsyncTasksRetryConfig($oTempConfig, $oP);
// Read the config from disk after save
$sConfigContent = file_get_contents($sConfigFile);
$sConfigChecksum = md5($sConfigContent);
$sConfig = str_replace("\r\n", "\n", $sConfigContent);
$sOriginalConfig = $sConfig;
}
}
catch (Exception $e) {
$oAlert = GetAlertFromException($e);
$oP->AddUiBlock($oAlert);
}
if (!empty($sOperation)) {
$iEditorTopMargin += 5;
$sConfig = utils::ReadParam('new_config', '', false, 'raw_data');
$sOriginalConfig = utils::ReadParam('prev_config', '', false, 'raw_data');
}
// (remove EscapeHtml) N°5914 - Wrong encoding in modules configuration editor
$oP->AddUiBlock(new Html('<p>'.Dict::S('config-edit-intro').'</p>'));
if ($sOperation == 'revert') {
$iEditorTopMargin += 5;
$oAlert = AlertUIBlockFactory::MakeForWarning('', Dict::S('config-reverted'));
$oP->AddUiBlock($oAlert);
}
if ($sOperation == 'save') {
$sTransactionId = utils::ReadParam('transaction_id', '', false, 'transaction_id');
if (!utils::IsTransactionValid($sTransactionId, true)) {
$iEditorTopMargin += 5;
$oAlert = AlertUIBlockFactory::MakeForFailure('', 'Error: invalid Transaction ID. The configuration was <b>NOT</b> modified.');
$oP->AddUiBlock($oAlert);
} else {
if ($sConfig == $sOriginalConfig) {
$iEditorTopMargin += 5;
$oAlert = AlertUIBlockFactory::MakeForInformation('', Dict::S('config-no-change'));
$oP->AddUiBlock($oAlert);
} else {
try {
TestConfig($sConfig, $oP); // throws exceptions
$oForm = new Form();
$oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden('operation', 'save', 'operation'));
$oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden('transaction_id', utils::GetNewTransactionId()));
$oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden('checksum', $sConfigChecksum));
@chmod($sConfigFile, 0770); // Allow overwriting the file
$sTmpFile = tempnam(SetupUtils::GetTmpDir(), 'itop-cfg-');
// Don't write the file as-is since it would allow to inject any kind of PHP code.
// Instead write the interpreted version of the file
// Note:
// The actual raw PHP code will anyhow be interpreted exactly twice: once in TestConfig() above
// and a second time during the load of the Config object below.
// If you are really concerned about an iTop administrator crafting some malicious
// PHP code inside the config file, then turn off the interactive configuration
// editor by adding the configuration parameter:
// 'itop-config' => array(
// 'config_editor' => 'disabled',
// )
file_put_contents($sTmpFile, $sConfig);
$oTempConfig = new Config($sTmpFile, true);
$oTempConfig->WriteToFile($sConfigFile);
@unlink($sTmpFile);
@chmod($sConfigFile, 0440); // Read-only
//--- Cancel button
$oCancelButton = ButtonUIBlockFactory::MakeForCancel(Dict::S('config-cancel'), 'cancel_button', null, true, 'cancel_button');
$oCancelButton->SetOnClickJsCode("return ResetConfig();");
$oForm->AddSubBlock($oCancelButton);
if (DBPasswordInNewConfigIsOk($sConfig)) {
$oAlert = AlertUIBlockFactory::MakeForSuccess('', Dict::S('config-saved'));
$iEditorTopMargin += 5;
} else {
$oAlert = AlertUIBlockFactory::MakeForInformation('', Dict::S('config-saved-warning-db-password'));
$iEditorTopMargin += 5;
}
$oP->AddUiBlock($oAlert);
//--- Submit button
$oSubmitButton = ButtonUIBlockFactory::MakeForPrimaryAction(Dict::S('config-apply'), null, Dict::S('config-apply'), true, 'submit_button');
$oForm->AddSubBlock($oSubmitButton);
$iWarnings = CheckAsyncTasksRetryConfig($oTempConfig, $oP);
$iEditorTopMargin += 5*$iWarnings;
//--- Config editor
$oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden('prev_config', $sOriginalConfig, 'prev_config'));
$oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden('new_config', $sOriginalConfig));
$oForm->AddHtml("<div id =\"new_config\" style=\"position: absolute; top: ".$iEditorTopMargin."em; bottom: 0; left: 5px; right: 5px;\"></div>");
$oP->AddUiBlock($oForm);
$sOriginalConfig = str_replace("\r\n", "\n", file_get_contents($sConfigFile));
}
catch (Exception $e) {
$oAlert = AlertUIBlockFactory::MakeForDanger('', $e->getMessage());
$iEditorTopMargin += 5;
$oP->AddUiBlock($oAlert);
}
}
}
}
// (remove EscapeHtml) N°5914 - Wrong encoding in modules configuration editor
$oP->AddUiBlock(new Html('<p>'.Dict::S('config-edit-intro').'</p>'));
$oForm = new Form();
$oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden('operation', 'save'));
$oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden('transaction_id', utils::GetNewTransactionId()));
//--- Cancel button
$oCancelButton = ButtonUIBlockFactory::MakeForCancel(Dict::S('config-cancel'), 'cancel_button', null, true, 'cancel_button');
$oCancelButton->SetOnClickJsCode("return ResetConfig();");
$oForm->AddSubBlock($oCancelButton);
//--- Submit button
$oSubmitButton = ButtonUIBlockFactory::MakeForPrimaryAction(Dict::S('config-apply'), null, Dict::S('config-apply'), true, 'submit_button');
$oForm->AddSubBlock($oSubmitButton);
//--- Config editor
$oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden('prev_config', $sOriginalConfig, 'prev_config'));
$oForm->AddSubBlock(InputUIBlockFactory::MakeForHidden('new_config', $sConfig));
$oForm->AddHtml("<div id =\"new_config\" style=\"position: absolute; top: ".$iEditorTopMargin."em; bottom: 0; left: 5px; right: 5px;\"></div>");
$oP->AddUiBlock($oForm);
$oP->add_script(
<<<'JS'
$oP->add_script(
<<<'JS'
var EditorUtils = (function() {
var STORAGE_RANGE_KEY = 'cfgEditorRange';
var STORAGE_LINE_KEY = 'cfgEditorFirstline';
@@ -271,9 +297,8 @@ var EditorUtils = (function() {
};
})();
JS
);
$oP->add_ready_script(
<<<'JS'
);
$oP->add_ready_script(<<<'JS'
var editor = ace.edit("new_config");
var configurationSource = $('input[name="new_config"]');
@@ -314,31 +339,25 @@ editorForm.on('submit', function() {
EditorUtils.restoreEditorDisplay(editor);
editor.focus();
JS
);
);
$sConfirmCancel = addslashes(Dict::S('config-confirm-cancel'));
$oP->add_script(<<<JS
$sConfirmCancel = addslashes(Dict::S('config-confirm-cancel'));
$oP->add_script(<<<JS
function ResetConfig()
{
var editor = ace.edit("new_config");
$("#operation").attr('value', 'revert');
var prevConfig = $('#prev_config');
if (editor.getValue() != prevConfig.val())
if (confirm('$sConfirmCancel'))
{
if (confirm('$sConfirmCancel'))
{
$('input[name="new_config"]').val(prevConfig.val());
return true;
}
$('input[name="new_config"]').val(prevConfig.val());
return true;
}
return false;
}
JS
);
}
}
);
} catch (Exception $e) {
$oP->p('<b>'.$e->getMessage().'</b>');
$oAlert = GetAlertFromException($e);
$oP->AddUiBlock($oAlert);
}
$oP->output();

View File

@@ -36,4 +36,8 @@ Dict::Add('EN US', 'English', 'English', array(
'config-parse-error' => 'Line %2$d: %1$s.<br/>The file has NOT been updated.',
'config-current-line' => 'Editing line: %1$s',
'config-saved-warning-db-password' => 'Successfully recorded, but the backup won\'t work due to unsupported characters in the database password.',
'config-error-transaction' => 'Error: invalid Transaction ID. The configuration was <b>NOT</b> modified.',
'config-error-file-changed' => 'Error: The configuration was changed on the server, the configuration has not been saved',
'config-not-allowed-in-demo' => 'Sorry, '.ITOP_APPLICATION_SHORT.' is in <b>demonstration mode</b>: the configuration file cannot be edited.',
'config-interactive-not-allowed' => ITOP_APPLICATION_SHORT." interactive edition of the configuration as been disabled. See <code>'config_editor' => 'disabled'</code> in the configuration file.",
));

View File

@@ -8,7 +8,7 @@
Dict::Add('FR FR', 'French', 'Français', array(
'Menu:ConfigEditor' => 'Configuration Générale',
'config-edit-title' => 'Editeur du Fichier de Configuration',
'config-edit-title' => 'Éditeur du Fichier de Configuration',
'config-edit-intro' => 'Attention: une configuration incorrecte peut rendre '.ITOP_APPLICATION_SHORT.' inopérant pour tous les utilisateurs!',
'config-apply' => 'Enregistrer',
'config-apply-title' => 'Enregistrer (Ctrl+S)',
@@ -20,4 +20,8 @@ Dict::Add('FR FR', 'French', 'Français', array(
'config-parse-error' => 'Ligne %2$d: %1$s.<br/>Le fichier n\'a PAS été modifié.',
'config-current-line' => 'Ligne en édition : %1$s',
'config-saved-warning-db-password' => 'Configuration enregistrée. Les sauvegardes ne fonctionneront pas à cause du format du pot de passe de la base.',
'config-error-transaction' => "Erreur : La transaction n'est plus valide. Les modifications n'ont <b>PAS</b> été enregistrées.",
'config-error-file-changed' => "Erreur : La configuration a été modifiée sur le serveur par une autre action. Les modifications n'ont <b>PAS</b> été enregistrées.",
'config-not-allowed-in-demo' => 'Désolé, '.ITOP_APPLICATION_SHORT.' est en <b>mode démonstration</b> : la configuration ne peut pas être modifiée.',
'config-interactive-not-allowed' => "La modification interactive de la configuration n'est pas autorisée. Voir le paramètre <code>'config_editor' => 'disabled'</code> dans le fichier de configuration.",
));