diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6119917f226a5a16fd4103165beb5861724de6a1..bbaf00ace8388fc2562ffde1feaa81bfdaddfd29 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,5 +1,10 @@ variables: - TO_PACKAGE: 'appinfo l10n lib templates js img' + CONTAINER_IMAGE: ubuntu + CONTAINER_TAG: focal + CONTAINER_NAME: nextcloud + APP_NAME: $CI_PROJECT_NAME + APP_ENABLE_ARGS: '' + TO_PACKAGE: 'appinfo l10n lib templates js img' include: - project: "e/infra/ecloud/nextcloud-apps/ci-templates" ref: main @@ -7,3 +12,43 @@ include: - project: "e/infra/ecloud/nextcloud-apps/ci-templates" ref: main file: "nc-apps-deploy.yml" + +.deploy:nextcloud-app: + stage: deploy + # assuming all deployment will happen with sames image + image: $CONTAINER_IMAGE:$CONTAINER_TAG + # assuming we will need to add SSH for all deployment + before_script: + - echo "FAIL" > .job_status + - mkdir $HOME/.ssh + - chmod 700 ~/.ssh + - echo "$SSH_PRIVATE_KEY_ED" > $HOME/.ssh/id_ed25519 + - echo "$SSH_PUBKEY_ED" > $HOME/.ssh/id_ed25519.pub + - echo "$SSH_KNOWN_HOSTS" > $HOME/.ssh/known_hosts + - chmod 600 ~/.ssh/id_ed25519 + - chmod 644 ~/.ssh/known_hosts ~/.ssh/id_ed25519.pub + - apt-get update && apt-get install -y openssh-client rsync + script: + - echo "Deploying ${APP_NAME} to $CI_ENVIRONMENT_NAME ($DEPLOYMENT_HOST)" + - rsync -avzh dist/ $SSH_USER@$DEPLOYMENT_HOST:/tmp/${CI_JOB_ID} + - ssh $SSH_USER@$DEPLOYMENT_HOST "sudo docker exec -u www-data $CONTAINER_NAME /usr/local/bin/php /var/www/html/occ app:disable ${APP_NAME} && + sudo rsync -avzh --chown www-data:www-data --delete /tmp/${CI_JOB_ID}/${APP_NAME} ${DEPLOYMENT_PATH}/html/custom_apps/ && + sudo docker exec -u www-data $CONTAINER_NAME /usr/local/bin/php /var/www/html/occ app:enable ${APP_ENABLE_ARGS} ${APP_NAME}" + - echo "SUCCESS" > .job_status + after_script: + # reading job status, checking it and implementing additional steps + # are not handled here as rm -rf /tmp/${CI_JOB_ID} will always execute + - ssh $SSH_USER@$DEPLOYMENT_HOST "rm -rf /tmp/${CI_JOB_ID}" + +deploy:staging: + extends: .deploy:nextcloud-app + when: manual + only: + - main + - murena-main + - production + - dev/migrate-webmail-contacts + - tags + environment: + name: staging/01 + url: https://eeo.one diff --git a/appinfo/info.xml b/appinfo/info.xml index 367f746de656c40d52f4788e3cf3baa365055207..97353b22a08dc7b171edbca9f6c96ee7412195ea 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -26,5 +26,6 @@ OCA\EcloudAccounts\Command\Migrate2FASecrets + OCA\EcloudAccounts\Command\MigrateWebmailAddressbooks diff --git a/lib/Command/Migrate2FASecrets.php b/lib/Command/Migrate2FASecrets.php index 13bddf3618e47057f81a3527512d2e4eaee2d9e1..1dd8cb389d616fda4e6f13177bafdd7b0bf85e88 100644 --- a/lib/Command/Migrate2FASecrets.php +++ b/lib/Command/Migrate2FASecrets.php @@ -61,7 +61,7 @@ class Migrate2FASecrets extends Command { foreach ($entries as $entry) { try { $this->ssoMapper->migrateCredential($entry['username'], $entry['secret']); - } catch(\Exception $e) { + } catch (\Exception $e) { $this->commandOutput->writeln('Error inserting entry for user ' . $entry['username'] . ' message: ' . $e->getMessage()); continue; } diff --git a/lib/Command/MigrateWebmailAddressbooks.php b/lib/Command/MigrateWebmailAddressbooks.php new file mode 100644 index 0000000000000000000000000000000000000000..4f089fb80d9340920eea7c72e80143d5f444227e --- /dev/null +++ b/lib/Command/MigrateWebmailAddressbooks.php @@ -0,0 +1,102 @@ +webmailMapper = $webmailMapper; + $this->userManager = $userManager; + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('ecloud-accounts:migrate-webmail-addressbooks') + ->setDescription('Migrates Webmail addressbooks to cloud') + ->addOption( + 'users', + null, + InputOption::VALUE_OPTIONAL, + 'comma separated list of users', + '' + ) + ->addOption( + 'limit', + null, + InputOption::VALUE_OPTIONAL, + 'Limit of users to migrate', + null + ) + ->addOption( + 'offset', + null, + InputOption::VALUE_OPTIONAL, + 'Offset', + 0 + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + try { + $this->commandOutput = $output; + $usernames = []; + $usernameList = $input->getOption('users'); + if (!empty($usernameList)) { + $usernames = explode(',', $usernameList); + } + $limit = (int) $input->getOption('limit'); + $offset = (int) $input->getOption('offset'); + $this->migrateUsers($limit, $offset, $usernames); + return 0; + } catch (\Exception $e) { + $this->commandOutput->writeln($e->getMessage()); + return 1; + } + } + + /** + * Migrate user secrets to the SSO database + * + * @return void + */ + private function migrateUsers(int $limit, int $offset = 0, array $usernames = []) : void { + $users = []; + if (!empty($usernames)) { + $emails = []; + foreach ($usernames as $username) { + $user = $this->userManager->get($username); + if (!$user instanceof IUser) { + $this->commandOutput->writeln('User ' . $username . ' does not exist!'); + continue; + } + + $email = $user->getEMailAddress(); + $emails[] = $email; + } + + + $users = $this->webmailMapper->getUsers($limit, $offset, $emails); + if (empty($users)) { + return; + } + $this->webmailMapper->migrateContacts($users); + return; + } + $users = $this->webmailMapper->getUsers($limit, $offset); + $this->webmailMapper->migrateContacts($users); + } +} diff --git a/lib/Db/SSOMapper.php b/lib/Db/SSOMapper.php index d0f0c69cd93b489bb0a80f3de1a6503ba30aa926..a879660db0570c7d47d220ca0760ae039448a95f 100644 --- a/lib/Db/SSOMapper.php +++ b/lib/Db/SSOMapper.php @@ -170,12 +170,12 @@ class SSOMapper { return $params; } - /** - * From https://www.uuidgenerator.net/dev-corner/php - * As keycloak generates random UUIDs using the java.util.UUID class which is RFC 4122 compliant - * - * @return string - */ + /** + * From https://www.uuidgenerator.net/dev-corner/php + * As keycloak generates random UUIDs using the java.util.UUID class which is RFC 4122 compliant + * + * @return string + */ private function randomUUID($data = null) : string { // Generate 16 bytes (128 bits) of random data or use the data passed into the function. $data = $data ?? random_bytes(16); diff --git a/lib/Db/WebmailMapper.php b/lib/Db/WebmailMapper.php new file mode 100644 index 0000000000000000000000000000000000000000..1c524cbfe1d0c9aefff5d4d894f6da46ceb2211e --- /dev/null +++ b/lib/Db/WebmailMapper.php @@ -0,0 +1,182 @@ +config = $config; + $this->logger = $logger; + $this->cardDavBackend = $cardDavBackend; + $this->userManager = $userManager; + if (!empty($this->config->getSystemValue(self::WEBMAIL_DB_CONFIG_KEY))) { + $this->initConnection(); + } + } + + private function isDbConfigValid($config) : bool { + if (!$config || !is_array($config)) { + return false; + } + if (!isset($config['db_port'])) { + $config['db_port'] = 3306; + } + + return isset($config['db_name']) + && isset($config['db_user']) + && isset($config['db_password']) + && isset($config['db_host']) + && isset($config['db_port']) ; + } + + + public function getUsers(int $limit, int $offset = 0, array $emails = []) : array { + $qb = $this->conn->createQueryBuilder(); + $qb->select('rl_email, id_user') + ->from(self::USERS_TABLE, 'u') + ->setFirstResult($offset); + if ($limit) { + $qb->setMaxResults($limit); + } + if (!empty($emails)) { + $qb->where('rl_email in (:emails)'); + $qb->setParameter('emails', $emails, IQueryBuilder::PARAM_STR_ARRAY); + } + + $result = $qb->execute(); + + $users = []; + while ($row = $result->fetch()) { + $user = [ + 'email' => $row['rl_email'], + 'id' => $row['id_user'] + ]; + $users[] = $user; + } + return $users; + } + + private function getUserContacts(string $uid) : array { + $qb = $this->conn->createQueryBuilder(); + + $qb->select('p.prop_value') + ->from('rainloop_ab_contacts', 'c') + ->where('c.id_user = :uid') + ->andWhere('p.prop_value IS NOT NULL') + ->setParameter('uid', $uid) + ->leftJoin('c', 'rainloop_ab_properties', 'p', 'p.id_contact = c.id_contact AND p.prop_type = 251'); + + $result = $qb->execute(); + $contacts = []; + while ($row = $result->fetch()) { + $contacts[] = Reader::readJson($row['prop_value']); + } + return $contacts; + } + + private function createCloudAddressBook(array $contacts, string $email) { + $users = $this->userManager->getByEmail($email); + $user = $users[0]; + + if (!$user instanceof IUser) { + return; + } + + $username = $user->getUID(); + + $principalUri = 'principals/users/'. $username; + $addressbookUri = 'webmail'; // some unique identifier + try { + $alreadyImported = $this->cardDavBackend->getAddressBooksByUri($principalUri, $addressbookUri); + + if ($alreadyImported) { + return; + } + + $addressBookId = $this->cardDavBackend->createAddressBook( + $principalUri, + $addressbookUri, + [ + '{DAV:}displayname' => 'Webmail', + '{urn:ietf:params:xml:ns:carddav}addressbook-description' => 'Contacts imported from snappymail' + ] + ); + } catch (Throwable $e) { + $this->logger->error('Error creating address book for user: ' . $username . ' ' . $e->getMessage()); + } + foreach ($contacts as $contact) { + try { + $contact->PRODID = '-//IDN murena.io//Migrated contact//EN'; + + $this->cardDavBackend->createCard( + $addressBookId, + UUIDUtil::getUUID() . '.vcf', + $contact->serialize(), + true + ); + } catch (Throwable $e) { + $this->logger->error('Error inserting contact for user: ' . $username . ' contact: ' . $contact->serialize() . ' ' . $e->getMessage()); + } + } + } + + public function migrateContacts(array $users) { + foreach ($users as $user) { + $contacts = $this->getUserContacts($user['id']); + if (!count($contacts)) { + return; + } + $this->createCloudAddressBook($contacts, $user['email']); + } + } + + + private function initConnection() : void { + try { + $params = $this->getConnectionParams(); + $this->conn = DriverManager::getConnection($params); + } catch (Throwable $e) { + $this->logger->error('Error connecting to Webmail database: ' . $e->getMessage()); + } + } + + private function getConnectionParams() : array { + $config = $this->config->getSystemValue(self::WEBMAIL_DB_CONFIG_KEY); + + if (!$this->isDbConfigValid($config)) { + throw new DbConnectionParamsException('Invalid Webmail database configuration!'); + } + + $params = [ + 'dbname' => $config['db_name'], + 'user' => $config['db_user'], + 'password' => $config['db_password'], + 'host' => $config['db_host'], + 'port' => $config['db_port'], + 'driver' => 'pdo_mysql' + ]; + return $params; + } +}