diff options
Diffstat (limited to 'modules/migrate')
5 files changed, 828 insertions, 0 deletions
diff --git a/modules/migrate/application/clicommands/ConfigCommand.php b/modules/migrate/application/clicommands/ConfigCommand.php new file mode 100644 index 0000000..a5be144 --- /dev/null +++ b/modules/migrate/application/clicommands/ConfigCommand.php @@ -0,0 +1,119 @@ +<?php +/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Migrate\Clicommands; + +use Icinga\Cli\Command; +use Icinga\Module\Migrate\Config\UserDomainMigration; +use Icinga\User; +use Icinga\Util\StringHelper; + +class ConfigCommand extends Command +{ + /** + * Rename users and user configurations according to a given domain + * + * The following configurations are taken into account: + * - Announcements + * - Preferences + * - Dashboards + * - Custom navigation items + * - Role configuration + * - Users and group memberships in database backends, if configured + * + * USAGE: + * + * icingacli migrate config users [options] + * + * OPTIONS: + * + * --to-domain=<to-domain> The new domain for the users + * + * --from-domain=<from-domain> Migrate only the users with the given domain. + * Use this switch in combination with --to-domain. + * + * --user=<user> Migrate only the given user in the format <user> or <user@domain> + * + * --map-file=<mapfile> File to use for renaming users + * + * --separator=<separator> Separator for the map file + * + * EXAMPLES: + * + * icingacli migrate config users ... + * + * Add the domain "icinga.com" to all users: + * + * --to-domain icinga.com + * + * Set the domain "example.com" on all users that have the domain "icinga.com": + * + * --to-domain example.com --from-domain icinga.com + * + * Set the domain "icinga.com" on the user "icingaadmin": + * + * --to-domain icinga.com --user icingaadmin + * + * Set the domain "icinga.com" on the users "icingaadmin@icinga.com" + * + * --to-domain example.com --user icingaadmin@icinga.com + * + * Rename users according to a map file: + * + * --map-file /path/to/mapfile --separator : + * + * MAPFILE: + * + * You may rename users according to a given map file. The map file must be separated by newlines. Each line then + * is specified in the format <from><separator><to>. The separator is specified with the --separator switch. + * + * Example content: + * + * icingaadmin:icingaadmin@icinga.com + * jdoe@example.com:jdoe@icinga.com + * rroe@icinga:rroe@icinga.com + */ + public function usersAction() + { + if ($this->params->has('map-file')) { + $mapFile = $this->params->get('map-file'); + $separator = $this->params->getRequired('separator'); + + $source = trim(file_get_contents($mapFile)); + $source = StringHelper::trimSplit($source, "\n"); + + $map = array(); + + array_walk($source, function ($item) use ($separator, &$map) { + list($from, $to) = StringHelper::trimSplit($item, $separator, 2); + $map[$from] = $to; + }); + + $migration = UserDomainMigration::fromMap($map); + } else { + $toDomain = $this->params->getRequired('to-domain'); + $fromDomain = $this->params->get('from-domain'); + $user = $this->params->get('user'); + + if ($user === null) { + $migration = UserDomainMigration::fromDomains($toDomain, $fromDomain); + } else { + if ($fromDomain !== null) { + $this->fail( + "Ambiguous arguments: Can't use --user in combination with --from-domain." + . " Please use the user@domain syntax for the --user switch instead." + ); + } + + $user = new User($user); + + $migrated = clone $user; + $migrated->setDomain($toDomain); + + $migration = UserDomainMigration::fromMap(array($user->getUsername() => $migrated->getUsername())); + } + } + + $migration->migrate(); + } +} diff --git a/modules/migrate/application/clicommands/NavigationCommand.php b/modules/migrate/application/clicommands/NavigationCommand.php new file mode 100644 index 0000000..06fb2a8 --- /dev/null +++ b/modules/migrate/application/clicommands/NavigationCommand.php @@ -0,0 +1,195 @@ +<?php + +/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */ + +namespace Icinga\Module\Migrate\Clicommands; + +use Icinga\Application\Config; +use Icinga\Application\Icinga; +use Icinga\Application\Logger; +use Icinga\Cli\Command; +use Icinga\Data\ConfigObject; +use Icinga\Exception\NotReadableError; +use Icinga\Exception\NotWritableError; +use Icinga\Module\Icingadb\Compat\UrlMigrator; +use Icinga\Util\DirectoryIterator; +use Icinga\Web\Request; +use ipl\Web\Filter\QueryString; +use ipl\Web\Url; + +class NavigationCommand extends Command +{ + /** + * Migrate local user monitoring navigation items to the Icinga DB Web actions + * + * USAGE + * + * icingacli migrate navigation [options] + * + * OPTIONS: + * + * --user=<username> Migrate monitoring navigation items only for + * the given user. (Default *) + * + * --delete Remove the legacy files after successfully + * migrated the navigation items. + */ + public function indexAction() + { + $moduleManager = Icinga::app()->getModuleManager(); + if (! $moduleManager->hasEnabled('icingadb')) { + Logger::error('Icinga DB module is not enabled. Please verify that the module is installed and enabled.'); + return; + } + + $preferencesPath = Config::resolvePath('preferences'); + $sharedNavigation = Config::resolvePath('navigation'); + if (! file_exists($preferencesPath) && ! file_exists($sharedNavigation)) { + Logger::info('There are no local user navigation items to migrate'); + return; + } + + $rc = 0; + $user = $this->params->get('user'); + $directories = new DirectoryIterator($preferencesPath); + + foreach ($directories as $directory) { + $username = $user; + if ($username !== null && $directories->key() !== $username) { + continue; + } + + if ($username === null) { + $username = $directories->key(); + } + + $hostActions = $this->readFromIni($directory . '/host-actions.ini', $rc); + $serviceActions = $this->readFromIni($directory . '/service-actions.ini', $rc); + + Logger::info('Migrating monitoring navigation items for user "%s" to the Icinga DB Web actions', $username); + + if (! $hostActions->isEmpty()) { + $this->migrateNavigationItems($hostActions, $directory . '/icingadb-host-actions.ini', $rc); + } + + if (! $serviceActions->isEmpty()) { + $this->migrateNavigationItems($serviceActions, $directory . '/icingadb-service-actions.ini', $rc); + } + } + + // Start migrating shared navigation items + $hostActions = $this->readFromIni($sharedNavigation . '/host-actions.ini', $rc); + $serviceActions = $this->readFromIni($sharedNavigation . '/service-actions.ini', $rc); + + Logger::info('Migrating shared monitoring navigation items to the Icinga DB Web actions'); + + if (! $hostActions->isEmpty()) { + $this->migrateNavigationItems($hostActions, $sharedNavigation . '/icingadb-host-actions.ini', $rc); + } + + if (! $serviceActions->isEmpty()) { + $this->migrateNavigationItems($serviceActions, $sharedNavigation . '/icingadb-service-actions.ini', $rc); + } + + if ($rc > 0) { + Logger::error('Failed to migrate some monitoring navigation items'); + exit($rc); + } + + Logger::info('Successfully migrated all local user monitoring navigation items'); + } + + /** + * Migrate the given config to the given new config path + * + * @param Config $config + * @param string $path + * @param int $rc + */ + private function migrateNavigationItems($config, $path, &$rc) + { + $deleteLegacyFiles = $this->params->get('delete'); + $newConfig = $this->readFromIni($path, $rc); + $counter = 1; + + /** @var ConfigObject $configObject */ + foreach ($config->getConfigObject() as $configObject) { + // Change the config type from "host-action" to icingadb's new action + if (strpos($path, 'icingadb-host-actions') !== false) { + $configObject->type = 'icingadb-host-action'; + } else { + $configObject->type = 'icingadb-service-action'; + } + + $urlString = $configObject->get('url'); + if ($urlString !== null) { + $url = Url::fromPath($urlString, [], new Request()); + + try { + $urlString = UrlMigrator::transformUrl($url)->getAbsoluteUrl(); + $configObject->url = rawurldecode($urlString); + } catch (\InvalidArgumentException $err) { + // Do nothing + } + } + + $legacyFilter = $configObject->get('filter'); + if ($legacyFilter !== null) { + $filter = QueryString::parse($legacyFilter); + $filter = UrlMigrator::transformFilter($filter); + if ($filter !== false) { + $configObject->filter = rawurldecode(QueryString::render($filter)); + } else { + unset($configObject->filter); + } + } + + $section = $config->key(); + while ($newConfig->hasSection($section)) { + $section = $config->key() . $counter++; + } + + $newConfig->setSection($section, $configObject); + } + + try { + if (! $newConfig->isEmpty()) { + $newConfig->saveIni(); + + // Remove the legacy file only if explicitly requested + if ($deleteLegacyFiles) { + unlink($config->getConfigFile()); + } + } + } catch (NotWritableError $error) { + Logger::error('%s: %s', $error->getMessage(), $error->getPrevious()->getMessage()); + $rc = 256; + } + } + + /** + * Get the navigation items config from the given ini path + * + * @param string $path Absolute path of the ini file + * @param int $rc The return code used to exit the action + * + * @return Config + */ + private function readFromIni($path, &$rc) + { + try { + $config = Config::fromIni($path); + } catch (NotReadableError $error) { + if ($error->getPrevious() !== null) { + Logger::error('%s: %s', $error->getMessage(), $error->getPrevious()->getMessage()); + } else { + Logger::error($error->getMessage()); + } + + $config = new Config(); + $rc = 128; + } + + return $config; + } +} diff --git a/modules/migrate/application/clicommands/PreferencesCommand.php b/modules/migrate/application/clicommands/PreferencesCommand.php new file mode 100644 index 0000000..11d1edb --- /dev/null +++ b/modules/migrate/application/clicommands/PreferencesCommand.php @@ -0,0 +1,131 @@ +<?php +/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */ + +namespace Icinga\Module\Migrate\Clicommands; + +use Icinga\Application\Config; +use Icinga\Application\Logger; +use Icinga\Cli\Command; +use Icinga\Data\ConfigObject; +use Icinga\Data\ResourceFactory; +use Icinga\Exception\NotReadableError; +use Icinga\Exception\NotWritableError; +use Icinga\File\Ini\IniParser; +use Icinga\User; +use Icinga\User\Preferences\PreferencesStore; +use Icinga\Util\DirectoryIterator; + +class PreferencesCommand extends Command +{ + /** + * Migrate local INI user preferences to a database + * + * USAGE + * + * icingacli migrate preferences [options] + * + * OPTIONS: + * + * --resource=<resource-name> The resource to use, if no current database config backend is configured. + * --no-set-config-backend Do not set the given resource as config backend automatically + */ + public function indexAction() + { + $resource = Config::app()->get('global', 'config_resource'); + if (empty($resource)) { + $resource = $this->params->getRequired('resource'); + } + + $resourceConfig = ResourceFactory::getResourceConfig($resource); + if ($resourceConfig->db === 'mysql') { + $resourceConfig->charset = 'utf8mb4'; + } + + $connection = ResourceFactory::createResource($resourceConfig); + + $preferencesPath = Config::resolvePath('preferences'); + if (! file_exists($preferencesPath)) { + Logger::info('There are no local user preferences to migrate'); + return; + } + + $rc = 0; + + $preferenceDirs = new DirectoryIterator($preferencesPath); + foreach ($preferenceDirs as $preferenceDir) { + if (! is_dir($preferenceDir)) { + continue; + } + + $userName = basename($preferenceDir); + + Logger::info('Migrating INI preferences for user "%s" to database...', $userName); + + $dbStore = new PreferencesStore(new ConfigObject(['connection' => $connection]), new User($userName)); + + try { + $dbStore->load(); + $dbStore->save( + new User\Preferences( + $this->loadIniFile($preferencesPath, (new User($userName))->getUsername()) + ) + ); + } catch (NotReadableError $e) { + if ($e->getPrevious() !== null) { + Logger::error('%s: %s', $e->getMessage(), $e->getPrevious()->getMessage()); + } else { + Logger::error($e->getMessage()); + } + + $rc = 128; + } catch (NotWritableError $e) { + Logger::error('%s: %s', $e->getMessage(), $e->getPrevious()->getMessage()); + $rc = 256; + } + } + + if ($rc > 0) { + Logger::error('Failed to migrate some user preferences'); + exit($rc); + } + + if ($this->params->has('resource') && ! $this->params->has('no-set-config-backend')) { + $appConfig = Config::app(); + $globalConfig = $appConfig->getSection('global'); + $globalConfig['config_resource'] = $resource; + + try { + $appConfig->saveIni(); + } catch (NotWritableError $e) { + Logger::error('Failed to update general configuration: %s', $e->getMessage()); + exit(256); + } + } + + Logger::info('Successfully migrated all local user preferences to database'); + } + + private function loadIniFile(string $filePath, string $username): array + { + $preferences = []; + $preferencesFile = sprintf( + '%s/%s/config.ini', + $filePath, + strtolower($username) + ); + + if (file_exists($preferencesFile)) { + if (! is_readable($preferencesFile)) { + throw new NotReadableError( + 'Preferences INI file %s for user %s is not readable', + $preferencesFile, + $username + ); + } else { + $preferences = IniParser::parseIniFile($preferencesFile)->toArray(); + } + } + + return $preferences; + } +} diff --git a/modules/migrate/library/Migrate/Config/UserDomainMigration.php b/modules/migrate/library/Migrate/Config/UserDomainMigration.php new file mode 100644 index 0000000..855a0ab --- /dev/null +++ b/modules/migrate/library/Migrate/Config/UserDomainMigration.php @@ -0,0 +1,378 @@ +<?php +/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Migrate\Config; + +use Icinga\Application\Config; +use Icinga\Data\Db\DbConnection; +use Icinga\Data\Filter\Filter; +use Icinga\Data\ResourceFactory; +use Icinga\User; +use Icinga\Util\DirectoryIterator; +use Icinga\Util\StringHelper; +use Icinga\Web\Announcement\AnnouncementIniRepository; + +class UserDomainMigration +{ + protected $toDomain; + + protected $fromDomain; + + protected $map; + + public static function fromMap(array $map) + { + $static = new static(); + + $static->map = $map; + + return $static; + } + + public static function fromDomains($toDomain, $fromDomain = null) + { + $static = new static(); + + $static->toDomain = $toDomain; + $static->fromDomain = $fromDomain; + + return $static; + } + + protected function mustMigrate(User $user) + { + if ($user->getUsername() === '*') { + return false; + } + + if ($this->map !== null) { + return isset($this->map[$user->getUsername()]); + } + + if ($this->fromDomain !== null && $user->hasDomain() && $user->getDomain() !== $this->fromDomain) { + return false; + } + + return true; + } + + protected function migrateUser(User $user) + { + $migrated = clone $user; + + if ($this->map !== null) { + $migrated->setUsername($this->map[$user->getUsername()]); + } else { + $migrated->setDomain($this->toDomain); + } + + return $migrated; + } + + protected function migrateAnnounces() + { + $announces = new AnnouncementIniRepository(); + + $query = $announces->select(array('author')); + + if ($this->map !== null) { + $query->where('author', array_keys($this->map)); + } + + $migratedUsers = array(); + + foreach ($announces->select(array('author')) as $announce) { + $user = new User($announce->author); + + if (! $this->mustMigrate($user)) { + continue; + } + + if (isset($migratedUsers[$user->getUsername()])) { + continue; + } + + $migrated = $this->migrateUser($user); + + $announces->update( + 'announcement', + array('author' => $migrated->getUsername()), + Filter::where('author', $user->getUsername()) + ); + + $migratedUsers[$user->getUsername()] = true; + } + } + + protected function migrateDashboards() + { + $directory = Config::resolvePath('dashboards'); + + $migration = array(); + + if (DirectoryIterator::isReadable($directory)) { + foreach (new DirectoryIterator($directory) as $username => $path) { + $user = new User($username); + + if (! $this->mustMigrate($user)) { + continue; + } + + $migrated = $this->migrateUser($user); + + $migration[$path] = dirname($path) . '/' . $migrated->getUsername(); + } + + foreach ($migration as $from => $to) { + rename($from, $to); + } + } + } + + protected function migrateNavigation() + { + $directory = Config::resolvePath('navigation'); + + foreach (new DirectoryIterator($directory, 'ini') as $file) { + $config = Config::fromIni($file); + + foreach ($config as $navigation) { + $owner = $navigation->owner; + + if (! empty($owner)) { + $user = new User($owner); + + if ($this->mustMigrate($user)) { + $migrated = $this->migrateUser($user); + + $navigation->owner = $migrated->getUsername(); + } + } + + $users = $navigation->users; + + if (! empty($users)) { + $users = StringHelper::trimSplit($users); + + foreach ($users as &$username) { + $user = new User($username); + + if (! $this->mustMigrate($user)) { + continue; + } + + $migrated = $this->migrateUser($user); + + $username = $migrated->getUsername(); + } + + $navigation->users = implode(',', $users); + } + } + + $config->saveIni(); + } + } + + protected function migratePreferences() + { + $config = Config::app(); + + $resourceConfig = ResourceFactory::getResourceConfig($config->get('global', 'config_resource')); + if ($resourceConfig->db === 'mysql') { + $resourceConfig->charset = 'utf8mb4'; + } + + /** @var DbConnection $conn */ + $conn = ResourceFactory::createResource($resourceConfig); + + $query = $conn + ->select() + ->from('icingaweb_user_preference', array('username')) + ->group('username'); + + if ($this->map !== null) { + $query->applyFilter(Filter::matchAny(Filter::where('username', array_keys($this->map)))); + } + + $users = $query->fetchColumn(); + + $migration = array(); + + foreach ($users as $username) { + $user = new User($username); + + if (! $this->mustMigrate($user)) { + continue; + } + + $migrated = $this->migrateUser($user); + + $migration[$username] = $migrated->getUsername(); + } + + if (! empty($migration)) { + $conn->getDbAdapter()->beginTransaction(); + + foreach ($migration as $originalUsername => $username) { + $conn->update( + 'icingaweb_user_preference', + array('username' => $username), + Filter::where('username', $originalUsername) + ); + } + + $conn->getDbAdapter()->commit(); + } + } + + protected function migrateRoles() + { + $roles = Config::app('roles'); + + foreach ($roles as $role) { + $users = $role->users; + + if (empty($users)) { + continue; + } + + $users = StringHelper::trimSplit($users); + + foreach ($users as &$username) { + $user = new User($username); + + if (! $this->mustMigrate($user)) { + continue; + } + + $migrated = $this->migrateUser($user); + + $username = $migrated->getUsername(); + } + + $role->users = implode(',', $users); + } + + $roles->saveIni(); + } + + protected function migrateUsers() + { + foreach (Config::app('authentication') as $name => $config) { + if (strtolower($config->backend) !== 'db') { + continue; + } + + $resourceConfig = ResourceFactory::getResourceConfig($config->resource); + if ($resourceConfig->db === 'mysql') { + $resourceConfig->charset = 'utf8mb4'; + } + + /** @var DbConnection $conn */ + $conn = ResourceFactory::createResource($resourceConfig); + + $query = $conn + ->select() + ->from('icingaweb_user', array('name')) + ->group('name'); + + if ($this->map !== null) { + $query->applyFilter(Filter::matchAny(Filter::where('name', array_keys($this->map)))); + } + + $users = $query->fetchColumn(); + + $migration = array(); + + foreach ($users as $username) { + $user = new User($username); + + if (! $this->mustMigrate($user)) { + continue; + } + + $migrated = $this->migrateUser($user); + + $migration[$username] = $migrated->getUsername(); + } + + if (! empty($migration)) { + $conn->getDbAdapter()->beginTransaction(); + + foreach ($migration as $originalUsername => $username) { + $conn->update( + 'icingaweb_user', + array('name' => $username), + Filter::where('name', $originalUsername) + ); + } + + $conn->getDbAdapter()->commit(); + } + } + + foreach (Config::app('groups') as $name => $config) { + if (strtolower($config->backend) !== 'db') { + continue; + } + + $resourceConfig = ResourceFactory::getResourceConfig($config->resource); + if ($resourceConfig->db === 'mysql') { + $resourceConfig->charset = 'utf8mb4'; + } + + /** @var DbConnection $conn */ + $conn = ResourceFactory::createResource($resourceConfig); + + $query = $conn + ->select() + ->from('icingaweb_group_membership', array('username')) + ->group('username'); + + if ($this->map !== null) { + $query->applyFilter(Filter::matchAny(Filter::where('username', array_keys($this->map)))); + } + + $users = $query->fetchColumn(); + + $migration = array(); + + foreach ($users as $username) { + $user = new User($username); + + if (! $this->mustMigrate($user)) { + continue; + } + + $migrated = $this->migrateUser($user); + + $migration[$username] = $migrated->getUsername(); + } + + if (! empty($migration)) { + $conn->getDbAdapter()->beginTransaction(); + + foreach ($migration as $originalUsername => $username) { + $conn->update( + 'icingaweb_group_membership', + array('username' => $username), + Filter::where('username', $originalUsername) + ); + } + + $conn->getDbAdapter()->commit(); + } + } + } + + public function migrate() + { + $this->migrateAnnounces(); + $this->migrateDashboards(); + $this->migrateNavigation(); + $this->migratePreferences(); + $this->migrateRoles(); + $this->migrateUsers(); + } +} diff --git a/modules/migrate/module.info b/modules/migrate/module.info new file mode 100644 index 0000000..6eb2911 --- /dev/null +++ b/modules/migrate/module.info @@ -0,0 +1,5 @@ +Module: migrate +Version: 2.11.4 +Description: Migrate module + This module was introduced with the domain-aware authentication feature in version 2.5.0. + It helps you migrating users and user configurations according to a given domain. |