coolconfig: Add config migration tool

Usage:
coolconfig migrateconfig [--old-config-file=<path>]
  [--config-file=<path>] [--write]

Signed-off-by: Aron Budea <aron.budea@collabora.com>
Change-Id: I44e16bd07588887e01732b430e4c6e368a034cd1
pull/3953/head
Aron Budea 2022-01-08 17:38:21 +01:00 committed by Andras Timar
parent 9f4d9c3f7e
commit c6155a3513
3 changed files with 249 additions and 2 deletions

View File

@ -233,6 +233,7 @@ coolstress_SOURCES = tools/Stress.cpp \
$(shared_sources)
coolconfig_SOURCES = tools/Config.cpp \
tools/ConfigMigrationAssistant.cpp \
common/DummyTraceEventEmitter.cpp \
common/Crypto.cpp \
common/Log.cpp \

View File

@ -18,6 +18,7 @@
#include <openssl/evp.h>
#include <Poco/Exception.h>
#include <Poco/File.h>
#include <Poco/Util/Application.h>
#include <Poco/Util/HelpFormatter.h>
#include <Poco/Util/Option.h>
@ -72,10 +73,12 @@ class Config: public Application
public:
static std::string ConfigFile;
static std::string OldConfigFile;
static std::string SupportKeyString;
static bool SupportKeyStringProvided;
static std::uint64_t AnonymizationSalt;
static bool AnonymizationSaltProvided;
static bool Write;
protected:
void defineOptions(OptionSet&) override;
@ -91,11 +94,16 @@ std::string Config::ConfigFile =
#endif
"/coolwsd.xml";
std::string Config::OldConfigFile = "/etc/loolwsd/loolwsd.xml";
bool Config::Write = false;
std::string Config::SupportKeyString;
bool Config::SupportKeyStringProvided = false;
std::uint64_t Config::AnonymizationSalt = 0;
bool Config::AnonymizationSaltProvided = false;
int MigrateConfig(std::string, std::string, bool);
void Config::displayHelp()
{
HelpFormatter helpFormatter(options());
@ -111,6 +119,7 @@ void Config::displayHelp()
// Command list
std::cout << std::endl
<< "Commands: " << std::endl
<< " migrateconfig [--old-config-file=<path>] [--config-file=<path>] [--write]" << std::endl
<< " anonymize [string-1]...[string-n]" << std::endl
<< " set-admin-password" << std::endl
#if ENABLE_SUPPORT_KEY
@ -131,11 +140,15 @@ void Config::defineOptions(OptionSet& optionSet)
.required(false)
.repeatable(false)
.argument("path"));
optionSet.addOption(Option("old-config-file", "", "Specify configuration file path to migrate manually.")
.required(false)
.repeatable(false)
.argument("path"));
optionSet.addOption(Option("pwd-salt-length", "", "Length of the salt to use to hash password [set-admin-password].")
.required(false)
.repeatable(false).
argument("number"));
.repeatable(false)
.argument("number"));
optionSet.addOption(Option("pwd-iterations", "", "Number of iterations to do in PKDBF2 password hashing [set-admin-password].")
.required(false)
.repeatable(false)
@ -156,6 +169,10 @@ void Config::defineOptions(OptionSet& optionSet)
.required(false)
.repeatable(false)
.argument("salt"));
optionSet.addOption(Option("write", "", "Write migrated configuration.")
.required(false)
.repeatable(false));
}
void Config::handleOption(const std::string& optionName, const std::string& optionValue)
@ -170,6 +187,10 @@ void Config::handleOption(const std::string& optionName, const std::string& opti
{
ConfigFile = optionValue;
}
else if (optionName == "old-config-file")
{
OldConfigFile = optionValue;
}
else if (optionName == "pwd-salt-length")
{
unsigned len = std::stoi(optionValue);
@ -211,6 +232,10 @@ void Config::handleOption(const std::string& optionName, const std::string& opti
AnonymizationSaltProvided = true;
std::cout << "Anonymization Salt: [" << AnonymizationSalt << "]." << std::endl;
}
else if (optionName == "write")
{
Write = true;
}
}
int Config::main(const std::vector<std::string>& args)
@ -384,6 +409,34 @@ int Config::main(const std::vector<std::string>& args)
std::cout << '[' << args[i] << "]: " << Util::anonymizeUrl(args[i], AnonymizationSalt) << std::endl;
}
}
else if (args[0] == "migrateconfig")
{
std::cout << "Migrating old configuration from " << OldConfigFile << " to " << ConfigFile << "." << std::endl;
if (!Write)
std::cout << "This is a dry run, no changes are written to file." << std::endl;
std::cout << std::endl;
const std::string OldConfigMigrated = OldConfigFile + ".migrated";
Poco::File AlreadyMigrated(OldConfigMigrated);
if (AlreadyMigrated.exists())
{
std::cout << "Migration already performed, file " + OldConfigMigrated + " exists. Aborting." << std::endl;
}
else
{
const int Result = MigrateConfig(OldConfigFile, ConfigFile, Write);
if (Result == 0)
{
std::cout << "Successful migration." << std::endl;
if (Write)
{
Poco::File ConfigToRename(OldConfigFile);
ConfigToRename.renameTo(OldConfigMigrated);
}
}
else
std::cout << "Migration of old configuration failed." << std::endl;
}
}
else
{
std::cerr << "No such command, \"" << args[0] << '"' << std::endl;

View File

@ -0,0 +1,193 @@
#include <iostream>
#include <memory>
#include <sstream>
#include <string>
#include <Poco/Util/XMLConfiguration.h>
#include <Poco/Util/AbstractConfiguration.h>
#include <Poco/AutoPtr.h>
using Poco::Util::XMLConfiguration;
using Poco::Util::AbstractConfiguration;
static const std::set<std::string> multiElems {".net.post_allow.host", ".storage.wopi.host", ".logging.file.property", ".ssl.hpkp.pins"};
static const std::map<std::string, std::string> renamedElems { {"loleaflet_logging", "browser_logging"} };
static const std::map<std::string, std::string> specialDefault {
{".ssl.cert_file_path", "/etc/loolwsd/cert.pem"},
{".ssl.key_file_path", "/etc/loolwsd/key.pem"},
{".ssl.ca_file_path", "/etc/loolwsd/ca-chain.cert.pem"},
{".ssl.termination", "false"},
{".logging.file.property[@name=path]", "/var/log/loolwsd.log"} };
static const std::map<std::string, std::string> specialAttribute {
{".logging.file", "enable"},
{".trace_event", "enable"},
{".trace.path","compress"},
{".trace.path","snapshot"},
{".net.post_allow","allow"},
{".ssl.hpkp","enable"},
{".ssl.hpkp","report_only"},
{".ssl.hpkp.max_age","enable"},
{".ssl.hpkp.report_uri","enable"} };
// {".storage.filesystem","allow"}, // don't migrate this
// {".storage.wopi","allow"}, // and this
void MigrateLevel(const XMLConfiguration &sourceConfig, XMLConfiguration &targetConfig, bool write, const std::string sourceLevel)
{
Poco::Util::AbstractConfiguration::Keys subKeys;
sourceConfig.keys(sourceLevel, subKeys);
for (auto key: subKeys)
{
const std::string fullKey = sourceLevel + "." + key;
MigrateLevel(sourceConfig, targetConfig, write, fullKey);
}
if (subKeys.empty())
{
const std::string sourceElement = sourceConfig.getString(sourceLevel);
// Need to handle keys pointing to multiple elements separately, refer to multiElems
const std::string commonKeyPart =
sourceLevel.find("[") != std::string::npos ? sourceLevel.substr(0, sourceLevel.find("[")) : sourceLevel;
if (multiElems.find(commonKeyPart) != multiElems.end())
{
if (commonKeyPart == ".logging.file.property")
{
const std::string propName = sourceConfig.getString(sourceLevel + "[@name]");
const std::string targetKey = commonKeyPart + "[@name=" + propName + "]";
const std::string targetElement = targetConfig.getString(targetKey);
const bool hasSpecialDefault = specialDefault.find(targetKey) != specialDefault.end();
if (targetElement != sourceElement && (!hasSpecialDefault || sourceElement != specialDefault.at(targetKey)))
{
std::cout << targetKey << ": replaced \"" << targetElement << "\" with \"" << sourceElement << "\"." << std::endl;
targetConfig.setString(targetKey, sourceElement);
}
}
else if (commonKeyPart == ".net.post_allow.host" || commonKeyPart == ".storage.wopi.host")
{
bool foundKey = false;
int id = 0;
while (!foundKey)
{
const std::string targetKey(id == 0 ? commonKeyPart : commonKeyPart + "[" + std::to_string(id) + "]");
if (!targetConfig.has(targetKey))
{
break;
}
if (targetConfig.getString(targetKey) == sourceElement)
{
foundKey = true;
}
id++;
}
if (!foundKey)
{
const std::string targetKeyRoot(commonKeyPart + "[" + std::to_string(id) + "]");
std::cout << targetKeyRoot << ": added \"" << sourceElement << "\"." << std::endl;
targetConfig.setString(targetKeyRoot, sourceElement);
targetConfig.setString(targetKeyRoot + "[@desc]", sourceConfig.getString(sourceLevel + "[@desc]"));
// for WOPI host, copy "allow" attribute as well
if (commonKeyPart == ".storage.wopi.host")
{
targetConfig.setString(targetKeyRoot + "[@allow]", sourceConfig.getString(sourceLevel + "[@allow]"));
}
}
}
// generic handling of lists that are shipped empty
else if (commonKeyPart == ".ssl.hpkp.pins.pin" || commonKeyPart == ".monitors.monitor")
{
// Shipped empty, no need to check for existing, append new ones
int id = 0;
while (true)
{
const std::string targetKey(id == 0 ? commonKeyPart : commonKeyPart + "[" + std::to_string(id) + "]");
if (!targetConfig.has(targetKey))
{
break;
}
id++;
}
const std::string targetKey(id == 0 ? commonKeyPart : commonKeyPart + "[" + std::to_string(id) + "]");
std::cout << targetKey << ": added \"" << sourceElement << "\"." << std::endl;
targetConfig.setString(targetKey, sourceElement);
}
else
{
std::cerr << "Unhandled key with multiples: " << sourceLevel << std::endl;
}
return;
}
bool moveOn = false;
// Don't migrate empty elements
if (sourceElement == "")
{
moveOn = true;
}
const std::string targetKey = renamedElems.find(sourceLevel) != renamedElems.end() ? renamedElems.at(sourceLevel) : sourceLevel;
// Special default and normal default cases
if (!moveOn && specialDefault.find(sourceLevel) != specialDefault.end())
{
if (sourceElement != specialDefault.at(sourceLevel))
{
std::cout << targetKey << ": replaced \"" << targetConfig.getString(targetKey) << "\" with \"" << sourceElement << "\"." << std::endl;
targetConfig.setString(targetKey, sourceElement);
}
moveOn = true;
}
else if (!moveOn)
{
const std::string defaultAttr = sourceLevel + "[@default]";
// Nothing to migrate if the config is the same as the default (if a default exists)
if (sourceConfig.has(defaultAttr) && (sourceElement == sourceConfig.getString(defaultAttr)))
{
moveOn = true;
}
}
// If new config doesn't have the element anymore, disregard
if (!moveOn && !targetConfig.has(targetKey))
{
std::cout << targetKey << " does not exist, and is not relevant anymore." << std::endl;
moveOn = true;
}
// Finally, migrate if the source and target values are different
if (!moveOn && sourceElement != targetConfig.getString(targetKey))
{
const std::string targetElement = targetConfig.getString(targetKey);
// Don't log password
if (sourceLevel == ".admin_console.password")
std::cout << targetKey << ": replaced \"" << targetElement << "\" with \"******\"." << std::endl;
else
std::cout << targetKey << ": replaced \"" << targetElement << "\" with \"" << sourceElement << "\"." << std::endl;
targetConfig.setString(targetKey, sourceElement);
moveOn = true;
}
}
// Handle special attributes, can exist both for leaf nodes and elements with subnodes
if (specialAttribute.find(sourceLevel) != specialAttribute.end())
{
const std::string targetAttrKey =
(renamedElems.find(sourceLevel) != renamedElems.end() ? renamedElems.at(sourceLevel) : sourceLevel) +
"[@" + specialAttribute.at(sourceLevel) + "]";
const std::string sourceAttrKey = sourceLevel+ "[@" + specialAttribute.at(sourceLevel) + "]";
const std::string sourceAttribute = sourceConfig.getString(sourceAttrKey);
const std::string targetAttribute = targetConfig.getString(targetAttrKey);
if (sourceAttribute != targetAttribute)
{
std::cout << sourceAttrKey << ": replaced attribute \"" << targetAttribute << "\" with \""<< sourceAttribute << "\"." << std::endl;
targetConfig.setString(targetAttrKey, sourceAttribute);
}
}
}
int MigrateConfig(std::string oldConfigFile, std::string newConfigFile, bool write) {
Poco::AutoPtr<XMLConfiguration> oldXMLConfig(new XMLConfiguration(oldConfigFile));
Poco::AutoPtr<XMLConfiguration> newXMLConfig(new XMLConfiguration(newConfigFile));
MigrateLevel(*oldXMLConfig, *newXMLConfig, write, "");
if (write)
newXMLConfig->save(newConfigFile);
return 0;
}