From 8ca6cc32b2c789a3149861159ad258f2cb9491e3 Mon Sep 17 00:00:00 2001
From: Daniel Baumann <daniel.baumann@progress-linux.org>
Date: Sun, 28 Apr 2024 14:39:39 +0200
Subject: Adding upstream version 2.11.4.

Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
---
 .../application/clicommands/ConfigCommand.php      | 119 +++++++
 .../application/clicommands/NavigationCommand.php  | 195 +++++++++++
 .../application/clicommands/PreferencesCommand.php | 131 +++++++
 .../library/Migrate/Config/UserDomainMigration.php | 378 +++++++++++++++++++++
 modules/migrate/module.info                        |   5 +
 5 files changed, 828 insertions(+)
 create mode 100644 modules/migrate/application/clicommands/ConfigCommand.php
 create mode 100644 modules/migrate/application/clicommands/NavigationCommand.php
 create mode 100644 modules/migrate/application/clicommands/PreferencesCommand.php
 create mode 100644 modules/migrate/library/Migrate/Config/UserDomainMigration.php
 create mode 100644 modules/migrate/module.info

(limited to 'modules/migrate')

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.
-- 
cgit v1.2.3