Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit 8a70896e authored by Akhil's avatar Akhil
Browse files

Refactor

parent 1015d550
Loading
Loading
Loading
Loading
Loading
+128 −34
Original line number Diff line number Diff line
@@ -5,25 +5,34 @@ declare(strict_types=1);
namespace OCA\EcloudAccounts\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use OCA\TwoFactorTOTP\Db\TotpSecretMapper;
use OCP\Security\ICrypto;
use OCP\IDBConnection;
use OCP\IUserManager;
use OCP\IUser;
use OCA\EcloudAccounts\Db\SSOMapper;
use OCA\EcloudAccounts\Exception\DbConnectionParamsException;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Connection;

class Migrate2FASecrets extends Command {
	private TotpSecretMapper $totpSecretMapper;
	private SSOMapper $SSOMapper;
	private SSOMapper $ssoMapper;
	private IDBConnection $dbConn;
	private Connection $ssoDbConn;
	private ICrypto $crypto;
	private const TOTP_TABLE = 'twofactor_totp_secrets';
	private IUserManager $userManager;
	private const TOTP_SECRET_TABLE = 'twofactor_totp_secrets';

	public function __construct(TotpSecretMapper $totpSecretMapper, IDBConnection $dbConn, ICrypto $crypto, SSOMapper $SSOMapper) {
	public function __construct(TotpSecretMapper $totpSecretMapper, IDBConnection $dbConn, ICrypto $crypto, SSOMapper $ssoMapper, IUserManager $userManager) {
		$this->totpSecretMapper = $totpSecretMapper;
		$this->SSOMapper = $SSOMapper;
		$this->conn = $conn;
		$this->ssoMapper = $ssoMapper;
		$this->userManager = $userManager;
		$this->dbConn = $dbConn;
		$this->crypto = $crypto;
		parent::__construct();
	}

@@ -31,69 +40,154 @@ class Migrate2FASecrets extends Command {
		$this
				->setName('ecloud-accounts:migrate-2fa-secrets')
				->setDescription('Migrates 2FA secrets to SSO database')
				->addArgument(
				->addOption(
					'users',
					InputArgument::OPTIONAL,
					'comma separated list of users'
					null,
					InputOption::VALUE_OPTIONAL,
					'comma separated list of users',
					''
				)
				->addOption(
					'sso-db-name',
					null,
					InputOption::VALUE_REQUIRED,
					'SSO database name',
				)
				->addOption(
					'sso-db-user',
					null,
					InputOption::VALUE_REQUIRED,
					'SSO database user',
				)
				->addOption(
					'sso-db-password',
					null,
					InputOption::VALUE_REQUIRED,
					'SSO database password',
				)
				->addOption(
					'sso-db-host',
					null,
					InputOption::VALUE_REQUIRED,
					'SSO database host',
				)
				->addOption(
					'sso-db-port',
					null,
					InputOption::VALUE_REQUIRED,
					'SSO database port',
					3306
				);
	}

	protected function execute(InputInterface $input, OutputInterface $output): int {
		try {
			$users = explode(',', $input->getArgument('users'));
			$ssoSecretEntries = [];
			if (empty($users)) {
				$ssoSecretEntries = $this->getSSOSecretEntriesForAllUsers();
			} else {
				foreach ($users as $user) {
					$secret = $this->totpSecretMapper->getSecret($user);
					$ssoSecretEntries[] = $this->getSSOSecretEntry($user, );
			$dbName = $input->getOption('sso-db-name');
			$dbHost = $input->getOption('sso-db-host');
			$dbPort = $input->getOption('sso-db-port');
			$dbPassword = $input->getOption('sso-db-password');
			$dbUser = $input->getOption('sso-db-user');
			if (empty($dbName) || empty($dbHost) || empty($dbPort) || empty($dbPassword) || empty($dbUser)) {
				throw new DbConnectionParamsException('Invalid database parameters!');
			}

			$this->ssoDbConn = $this->getDatabaseConnection($dbName, $dbHost, $dbPort, $dbPassword, $dbUser);

			$usernames = [];
			$usernameList = $input->getOption('users');
			if (!empty($usernameList)) {
				$usernames = explode(',', $usernameList);
			}
			$this->SSOMapper->insertCredentials($ssoSecretEntries);

			$ssoSecretEntries = $this->getSSOSecretEntries($usernames);
			foreach ($ssoSecretEntries as $username => $entry) {
				try {
					$this->ssoMapper->insertCredential($entry, $this->ssoDbConn);
				} catch(\Exception $e) {
					$output->writeln('Error inserting entry for user ' . $username . ' message: ' . $e->getMessage());
					continue;
				}
			}
			return 0;
		} catch (\Exception $e) {
			$output->writeln($e->getMessage());
			return 1;
		}
	}

	private function getSSOSecretEntriesForAllUsers() : array {
	private function getSSOSecretEntries(array $usernames) : array {
		if (!empty($usernames)) {
			$entries = [];
		$query = $this->dbConn->getQueryBuilder();
			foreach ($usernames as $username) {
				$user = $this->userManager->get($username);
				if (!$user instanceof IUser) {
					continue;
				}
				$dbSecret = $this->totpSecretMapper->getSecret($user);
				$decryptedSecret = $this->crypto->decrypt($dbSecret->getSecret());
				$ssoUserId = $this->ssoMapper->getUserId($username, $this->ssoDbConn);
				$entries[$username] = $this->getSSOSecretEntry($decryptedSecret, $ssoUserId);
			}
			return $entries;
		}
		return $this->getAllSSOSecretEntries();
	}

	private function getAllSSOSecretEntries() : array {
		$entries = [];
		$qb = $this->dbConn->getQueryBuilder();
		$qb->select('user_id', 'secret')
		   ->from(self::TOTP_TABLE);
			->from(self::TOTP_SECRET_TABLE);
		$result = $qb->execute();
		while ($row = $result->fetch()) {
			$userId = (string) $result['user_id'];
			$secret = (string) $result['secret'];
			$username = (string) $row['user_id'];
			$secret = (string) $row['secret'];
			$decryptedSecret = $this->crypto->decrypt($secret);
			$entries[] = $this->getSSOSecretEntry($userId, $decryptedSecret);
			$ssoUserId = $this->ssoMapper->getUserId($username, $this->ssoDbConn);
			$entries[$username] = $this->getSSOSecretEntry($decryptedSecret, $ssoUserId);
		}
		return $entries;
	}

	private function getSSOSecretEntry(string $userId, string $secret) : array {
		$SSOUserId = $this->SSOMapper->getUserId($userId);

	private function getSSOSecretEntry(string $secret, string $ssoUserId) : array {
		$credentialEntry = [
			'ID' => $this->randomUUID(),
			'SALT' => null,
			'USER_ID' => $SSOUserId,
			'USER_ID' => $ssoUserId,
			'USER_LABEL' => 'Murena Cloud 2FA',
			'SECRET_DATA' => [
			'TYPE' => 'otp',
			'SECRET_DATA' => json_encode([
				'value' => $secret
			],
			'CREATED_DATE' => round(microtime(true) * 1000),
			'CREDENTIAL_DATA' => [
			]),
			'CREDENTIAL_DATA' => json_encode([
				'subType' => 'nextcloud_totp',
				'period' => 30,
				'digits' => 6,
				'algorithm' => 'HmacSHA1',
			],
			'priority' => 10
			]),
		];

		foreach ($credentialEntry as $key => &$value) {
			$value = "'" . $value . "'";
		}
		$credentialEntry['CREATED_DATE'] = round(microtime(true) * 1000);
		$credentialEntry['PRIORITY'] = 10;

		return $credentialEntry;
	}

	private function getDatabaseConnection(string $dbName, string $dbHost, int $dbPort, string $dbPassword, string $dbUser) {
		$params = [
			'dbname' => $dbName,
			'user' => $dbUser,
			'password' => $dbPassword,
			'host' => $dbHost,
			'port' => $dbPort,
			'driver' => 'pdo_mysql'
		];

		return  DriverManager::getConnection($params);
	}

	/*
		From https://www.uuidgenerator.net/dev-corner/php
+11 −57
Original line number Diff line number Diff line
@@ -4,12 +4,10 @@ namespace OCA\EcloudAccounts\Db;

use OCP\IConfig;
use OCP\ILogger;
use OCA\EcloudAccounts\Exception\DbConnectionParamsException;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Connection;

class SSOMapper {
	private $config;
	private $conn;
	private $logger;
	private const USER_ATTRIBUTE_TABLE = 'USER_ATTRIBUTE';
	private const CREDENTIAL_TABLE = 'CREDENTIAL';
@@ -17,68 +15,24 @@ class SSOMapper {
	public function __construct(IConfig $config, ILogger $logger) {
		$this->config = $config;
		$this->logger = $logger;
		$this->initConnection();
	}

	private function initConnection() {
		$params = $this->getConnectionParams();
		$this->conn = DriverManager::getConnection($params);
	}

	private function isDbConfigValid($config) : bool {
		if (!$config || !is_array($config)) {
			return false;
		}
		return isset($config['db_name'])
			&& isset($config['db_user'])
			&& isset($config['db_password'])
			&& isset($config['db_host'])
			&& isset($config['db_port']) ;
	}

	private function getConnectionParams() {
		$config = $this->config->getSystemValue('sso_database');
		
		if (!$this->isDbConfigValid($config)) {
			throw new DbConnectionParamsException('Invalid SSO 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;
	}

	public function getUserId(string $username) : string {
		$qb = $this->conn->createQueryBuilder();
	public function getUserId(string $username, Connection $conn) : string {
		$qb = $conn->createQueryBuilder();
		$qb->select('USER_ID')
			->from(self::USER_ATTRIBUTE_TABLE)
			->where($qb->expr()->eq('NAME', 'LDAP_ID'))
			->where($qb->expr()->eq('VALUE', $qb->createParameter('username')));
			->where('NAME = "LDAP_ID"')
			->andWhere('VALUE = :username');
			
		$qb->setParameter('username', $username);

		$result = $qb->execute();
		$SSOUserId = (string) $result->fetchColumn();
		return $SSOUserId;
		return (string) $result->fetchOne();
	}

	public function insertCredentials(array $entries) {
		$qb = $this->conn->createQueryBuilder();
		foreach ($entries as $entry) {
			try {
	public function insertCredential(array $entry, Connection $conn) {
		$qb = $conn->createQueryBuilder();
		$qb->insert(self::CREDENTIAL_TABLE)
			->values($entry)
			->execute();
			} catch(Exception $e) {
				$this->logger->logException($e, ['Error migrating 2FA secret for SSO user ID ' . $entry['USER_ID']]);
				continue;
			}
		}
	}
}