From 469ce065d293496e0fc7962c1c842dea92322d1f Mon Sep 17 00:00:00 2001 From: Akhil Date: Sat, 25 Mar 2023 01:42:43 +0530 Subject: [PATCH 1/8] Add occ command for migrate step --- lib/Command/Migrate2FASecrets.php | 117 ++++++++++++++++++++++++++++++ lib/Db/SSOMapper.php | 73 +++++++++++++++++++ 2 files changed, 190 insertions(+) create mode 100644 lib/Command/Migrate2FASecrets.php create mode 100644 lib/Db/SSOMapper.php diff --git a/lib/Command/Migrate2FASecrets.php b/lib/Command/Migrate2FASecrets.php new file mode 100644 index 00000000..74eb6519 --- /dev/null +++ b/lib/Command/Migrate2FASecrets.php @@ -0,0 +1,117 @@ +totpSecretMapper = $totpSecretMapper; + $this->SSOMapper = $SSOMapper; + $this->conn = $conn; + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('ecloud-accounts:migrate-2fa-secrets') + ->setDescription('Migrates 2FA secrets to SSO database') + ->addArgument( + 'users', + InputArgument::OPTIONAL, + 'comma separated list of users' + ); + } + + 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, ); + } + } + } catch (\Exception $e) { + $output->writeln($e->getMessage()); + return 1; + } + } + + private function getSSOSecretEntriesForAllUsers() : array { + $entries = []; + $query = $this->dbConn->getQueryBuilder(); + $qb->select( 'user_id', 'secret') + ->from(self::TOTP_TABLE); + $result = $qb->execute(); + while ($row = $result->fetch()) { + $userId = (string) $result['user_id']; + $secret = (string) $result['secret']; + $decryptedSecret = $this->crypto->decrypt($secret); + $entries[] = $this->getSSOSecretEntry($userId, $secret); + } + return $entries; + + } + + private function getSSOSecretEntry(string $userId, string $secret) : array { + $SSOUserId = $this->SSOMapper->getUserId($userId); + $credentialEntry = [ + 'ID' => $this->randomUUID(), + 'SALT' => NULL, + 'USER_ID' => $SSOUserId, + 'USER_LABEL' => 'Murena Cloud 2FA', + 'SECRET_DATA' => [ + 'value' => $secret + ], + 'CREATED_DATE' => round(microtime(true) * 1000), + 'CREDENTIAL_DATA' => [ + 'subType' => 'nextcloud_totp', + 'period' => 30, + 'digits' => 6, + 'algorithm' => 'HmacSHA1', + ], + 'priority' => 10 + ]; + return $credentialEntry; + } + + + /* + From https://www.uuidgenerator.net/dev-corner/php + As keycloak generates random UUIDs using the java.util.UUID class which is RFC 4122 compliant + */ + private function randomUUID($data = null) { + // Generate 16 bytes (128 bits) of random data or use the data passed into the function. + $data = $data ?? random_bytes(16); + assert(strlen($data) == 16); + + // Set version to 0100 + $data[6] = chr(ord($data[6]) & 0x0f | 0x40); + // Set bits 6-7 to 10 + $data[8] = chr(ord($data[8]) & 0x3f | 0x80); + + // Output the 36 character UUID. + return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); + } + +} diff --git a/lib/Db/SSOMapper.php b/lib/Db/SSOMapper.php new file mode 100644 index 00000000..efac1f46 --- /dev/null +++ b/lib/Db/SSOMapper.php @@ -0,0 +1,73 @@ +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 { + $query = $this->conn->createQueryBuilder(); + $query->select('USER_ID') + ->from('USER_ATTRIBUTE') + ->where($query->expr()->eq('NAME', 'LDAP_ID')) + ->where($query->expr()->eq('VALUE', $query->createParameter('username'))); + + $query->setParameter('username', $username); + + $result = $query->execute(); + $SSOUserId = (string) $result->fetchColumn(); + return $SSOUserId; + } + + public function insertCredentials(array $entries) { + + } +} -- GitLab From 324dfd9da25c73679b94373c2d58e8db8c1c95ce Mon Sep 17 00:00:00 2001 From: Akhil Date: Sat, 25 Mar 2023 01:43:12 +0530 Subject: [PATCH 2/8] CS fix Signed-off-by: Akhil --- lib/Command/Migrate2FASecrets.php | 41 ++++++++++++++----------------- lib/Db/SSOMapper.php | 22 ++++++++--------- 2 files changed, 29 insertions(+), 34 deletions(-) diff --git a/lib/Command/Migrate2FASecrets.php b/lib/Command/Migrate2FASecrets.php index 74eb6519..d354f975 100644 --- a/lib/Command/Migrate2FASecrets.php +++ b/lib/Command/Migrate2FASecrets.php @@ -3,10 +3,10 @@ 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\InputInterface; -use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use OCA\TwoFactorTOTP\Db\TotpSecretMapper; use OCP\Security\ICrypto; @@ -14,7 +14,6 @@ use OCP\IDBConnection; use OCA\EcloudAccounts\Db\SSOMapper; class Migrate2FASecrets extends Command { - private TotpSecretMapper $totpSecretMapper; private SSOMapper $SSOMapper; private IDBConnection $dbConn; @@ -41,50 +40,49 @@ class Migrate2FASecrets extends Command { protected function execute(InputInterface $input, OutputInterface $output): int { try { - $users = explode(',' , $input->getArgument('users')); + $users = explode(',', $input->getArgument('users')); $ssoSecretEntries = []; - if(empty($users)) { + if (empty($users)) { $ssoSecretEntries = $this->getSSOSecretEntriesForAllUsers(); } else { - foreach($users as $user) { + foreach ($users as $user) { $secret = $this->totpSecretMapper->getSecret($user); $ssoSecretEntries[] = $this->getSSOSecretEntry($user, ); } } } catch (\Exception $e) { - $output->writeln($e->getMessage()); - return 1; + $output->writeln($e->getMessage()); + return 1; } } private function getSSOSecretEntriesForAllUsers() : array { $entries = []; $query = $this->dbConn->getQueryBuilder(); - $qb->select( 'user_id', 'secret') - ->from(self::TOTP_TABLE); - $result = $qb->execute(); - while ($row = $result->fetch()) { - $userId = (string) $result['user_id']; - $secret = (string) $result['secret']; - $decryptedSecret = $this->crypto->decrypt($secret); - $entries[] = $this->getSSOSecretEntry($userId, $secret); - } - return $entries; - + $qb->select('user_id', 'secret') + ->from(self::TOTP_TABLE); + $result = $qb->execute(); + while ($row = $result->fetch()) { + $userId = (string) $result['user_id']; + $secret = (string) $result['secret']; + $decryptedSecret = $this->crypto->decrypt($secret); + $entries[] = $this->getSSOSecretEntry($userId, $secret); + } + return $entries; } private function getSSOSecretEntry(string $userId, string $secret) : array { $SSOUserId = $this->SSOMapper->getUserId($userId); $credentialEntry = [ 'ID' => $this->randomUUID(), - 'SALT' => NULL, + 'SALT' => null, 'USER_ID' => $SSOUserId, 'USER_LABEL' => 'Murena Cloud 2FA', 'SECRET_DATA' => [ 'value' => $secret ], 'CREATED_DATE' => round(microtime(true) * 1000), - 'CREDENTIAL_DATA' => [ + 'CREDENTIAL_DATA' => [ 'subType' => 'nextcloud_totp', 'period' => 30, 'digits' => 6, @@ -96,7 +94,7 @@ class Migrate2FASecrets extends Command { } - /* + /* From https://www.uuidgenerator.net/dev-corner/php As keycloak generates random UUIDs using the java.util.UUID class which is RFC 4122 compliant */ @@ -113,5 +111,4 @@ class Migrate2FASecrets extends Command { // Output the 36 character UUID. return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); } - } diff --git a/lib/Db/SSOMapper.php b/lib/Db/SSOMapper.php index efac1f46..cb6c2ea5 100644 --- a/lib/Db/SSOMapper.php +++ b/lib/Db/SSOMapper.php @@ -6,7 +6,6 @@ use OCP\IConfig; use OCP\ILogger; use OCA\EcloudAccounts\Exception\DbConnectionParamsException; use Doctrine\DBAL\DriverManager; -use Throwable; class SSOMapper { private $config; @@ -54,20 +53,19 @@ class SSOMapper { } public function getUserId(string $username) : string { - $query = $this->conn->createQueryBuilder(); - $query->select('USER_ID') - ->from('USER_ATTRIBUTE') - ->where($query->expr()->eq('NAME', 'LDAP_ID')) - ->where($query->expr()->eq('VALUE', $query->createParameter('username'))); + $query = $this->conn->createQueryBuilder(); + $query->select('USER_ID') + ->from('USER_ATTRIBUTE') + ->where($query->expr()->eq('NAME', 'LDAP_ID')) + ->where($query->expr()->eq('VALUE', $query->createParameter('username'))); - $query->setParameter('username', $username); + $query->setParameter('username', $username); - $result = $query->execute(); - $SSOUserId = (string) $result->fetchColumn(); - return $SSOUserId; - } + $result = $query->execute(); + $SSOUserId = (string) $result->fetchColumn(); + return $SSOUserId; + } public function insertCredentials(array $entries) { - } } -- GitLab From 089ebd628b8150bfaabcf3eb43ad97c59a830afc Mon Sep 17 00:00:00 2001 From: Akhil Date: Mon, 3 Apr 2023 16:33:07 +0530 Subject: [PATCH 3/8] Add 2fa secret insert query --- lib/Command/Migrate2FASecrets.php | 3 ++- lib/Db/SSOMapper.php | 27 ++++++++++++++++++++------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/lib/Command/Migrate2FASecrets.php b/lib/Command/Migrate2FASecrets.php index d354f975..49ea557e 100644 --- a/lib/Command/Migrate2FASecrets.php +++ b/lib/Command/Migrate2FASecrets.php @@ -50,6 +50,7 @@ class Migrate2FASecrets extends Command { $ssoSecretEntries[] = $this->getSSOSecretEntry($user, ); } } + $this->SSOMapper->insertCredentials($ssoSecretEntries); } catch (\Exception $e) { $output->writeln($e->getMessage()); return 1; @@ -66,7 +67,7 @@ class Migrate2FASecrets extends Command { $userId = (string) $result['user_id']; $secret = (string) $result['secret']; $decryptedSecret = $this->crypto->decrypt($secret); - $entries[] = $this->getSSOSecretEntry($userId, $secret); + $entries[] = $this->getSSOSecretEntry($userId, $decryptedSecret); } return $entries; } diff --git a/lib/Db/SSOMapper.php b/lib/Db/SSOMapper.php index cb6c2ea5..4f6436be 100644 --- a/lib/Db/SSOMapper.php +++ b/lib/Db/SSOMapper.php @@ -11,6 +11,8 @@ class SSOMapper { private $config; private $conn; private $logger; + private const USER_ATTRIBUTE_TABLE = 'USER_ATTRIBUTE'; + private const CREDENTIAL_TABLE = 'CREDENTIAL'; public function __construct(IConfig $config, ILogger $logger) { $this->config = $config; @@ -53,19 +55,30 @@ class SSOMapper { } public function getUserId(string $username) : string { - $query = $this->conn->createQueryBuilder(); - $query->select('USER_ID') - ->from('USER_ATTRIBUTE') - ->where($query->expr()->eq('NAME', 'LDAP_ID')) - ->where($query->expr()->eq('VALUE', $query->createParameter('username'))); + $qb = $this->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'))); - $query->setParameter('username', $username); + $qb->setParameter('username', $username); - $result = $query->execute(); + $result = $qb->execute(); $SSOUserId = (string) $result->fetchColumn(); return $SSOUserId; } public function insertCredentials(array $entries) { + $qb = $this->conn->createQueryBuilder(); + foreach ($entries as $entry) { + try { + $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; + } + } } } -- GitLab From 1015d550cf0964cc651c6e4b2e60a963b0702622 Mon Sep 17 00:00:00 2001 From: Akhil Date: Wed, 5 Apr 2023 19:01:52 +0530 Subject: [PATCH 4/8] Add command to info.xml Signed-off-by: Akhil --- appinfo/info.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/appinfo/info.xml b/appinfo/info.xml index 28655290..def8000f 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -24,4 +24,7 @@ OCA\EcloudAccounts\Settings\BetaUserSetting OCA\EcloudAccounts\Settings\BetaSection + + OCA\EcloudAccounts\Command\Migrate2FASecrets + -- GitLab From 8a70896e251c58599e2c2422416131efe3de7354 Mon Sep 17 00:00:00 2001 From: Akhil Date: Fri, 7 Apr 2023 17:36:43 +0530 Subject: [PATCH 5/8] Refactor --- lib/Command/Migrate2FASecrets.php | 162 +++++++++++++++++++++++------- lib/Db/SSOMapper.php | 68 ++----------- 2 files changed, 139 insertions(+), 91 deletions(-) diff --git a/lib/Command/Migrate2FASecrets.php b/lib/Command/Migrate2FASecrets.php index 49ea557e..0e9cb132 100644 --- a/lib/Command/Migrate2FASecrets.php +++ b/lib/Command/Migrate2FASecrets.php @@ -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); + } + + $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; } } - $this->SSOMapper->insertCredentials($ssoSecretEntries); + 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 = []; + 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 = []; - $query = $this->dbConn->getQueryBuilder(); + $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 diff --git a/lib/Db/SSOMapper.php b/lib/Db/SSOMapper.php index 4f6436be..1e711eda 100644 --- a/lib/Db/SSOMapper.php +++ b/lib/Db/SSOMapper.php @@ -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 { - $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; - } - } + public function insertCredential(array $entry, Connection $conn) { + $qb = $conn->createQueryBuilder(); + $qb->insert(self::CREDENTIAL_TABLE) + ->values($entry) + ->execute(); } } -- GitLab From 9b914dcfd30f4ccde065a7ba3d4ab63947f4fd0d Mon Sep 17 00:00:00 2001 From: Akhil Date: Wed, 12 Apr 2023 18:48:53 +0530 Subject: [PATCH 6/8] Use single command to export --- lib/Command/Migrate2FASecrets.php | 103 +++++++++++++++--------------- 1 file changed, 51 insertions(+), 52 deletions(-) diff --git a/lib/Command/Migrate2FASecrets.php b/lib/Command/Migrate2FASecrets.php index 0e9cb132..7a63b67a 100644 --- a/lib/Command/Migrate2FASecrets.php +++ b/lib/Command/Migrate2FASecrets.php @@ -8,27 +8,24 @@ use Symfony\Component\Console\Command\Command; 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 IDBConnection $dbConn; private Connection $ssoDbConn; private ICrypto $crypto; private IUserManager $userManager; + private OutputInterface $commandOutput; private const TOTP_SECRET_TABLE = 'twofactor_totp_secrets'; - public function __construct(TotpSecretMapper $totpSecretMapper, IDBConnection $dbConn, ICrypto $crypto, SSOMapper $ssoMapper, IUserManager $userManager) { - $this->totpSecretMapper = $totpSecretMapper; + public function __construct(IDBConnection $dbConn, ICrypto $crypto, SSOMapper $ssoMapper, IUserManager $userManager) { $this->ssoMapper = $ssoMapper; $this->userManager = $userManager; $this->dbConn = $dbConn; @@ -82,73 +79,64 @@ class Migrate2FASecrets extends Command { protected function execute(InputInterface $input, OutputInterface $output): int { try { + $this->commandOutput = $output; $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); } - - $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; - } - } + $this->migrateUsers($usernames); return 0; } catch (\Exception $e) { - $output->writeln($e->getMessage()); + $this->commandOutput->writeln($e->getMessage()); return 1; } } - private function getSSOSecretEntries(array $usernames) : array { - if (!empty($usernames)) { - $entries = []; - 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 { + /** + * Migrate user secrets to the SSO database + * + * @return void + */ + private function migrateUsers(array $usernames = []) : void { $entries = []; $qb = $this->dbConn->getQueryBuilder(); $qb->select('user_id', 'secret') ->from(self::TOTP_SECRET_TABLE); + + if (!empty($usernames)) { + $qb->where('user_id IN (:usernames)') + ->setParameter('usernames', implode(',', $usernames)); + } + $result = $qb->execute(); while ($row = $result->fetch()) { - $username = (string) $row['user_id']; - $secret = (string) $row['secret']; - $decryptedSecret = $this->crypto->decrypt($secret); - $ssoUserId = $this->ssoMapper->getUserId($username, $this->ssoDbConn); - $entries[$username] = $this->getSSOSecretEntry($decryptedSecret, $ssoUserId); + try { + $username = (string) $row['user_id']; + $secret = (string) $row['secret']; + $decryptedSecret = $this->crypto->decrypt($secret); + $ssoUserId = $this->ssoMapper->getUserId($username, $this->ssoDbConn); + $entry = $this->getSSOSecretEntry($decryptedSecret, $ssoUserId); + $this->ssoMapper->insertCredential($entry, $this->ssoDbConn); + } catch(\Exception $e) { + $this->commandOutput->writeln('Error inserting entry for user ' . $username . ' message: ' . $e->getMessage()); + continue; + } } - return $entries; } + /** + * Create secret entry compatible with Keycloak schema + * + * @return array + */ private function getSSOSecretEntry(string $secret, string $ssoUserId) : array { $credentialEntry = [ @@ -176,7 +164,16 @@ class Migrate2FASecrets extends Command { return $credentialEntry; } - private function getDatabaseConnection(string $dbName, string $dbHost, int $dbPort, string $dbPassword, string $dbUser) { + /** + * Attempt to connect to a non-NC database + * + * @return Connection + */ + private function getDatabaseConnection(string $dbName, string $dbHost, int $dbPort, string $dbPassword, string $dbUser) : Connection { + if (empty($dbName) || empty($dbHost) || empty($dbPort) || empty($dbPassword) || empty($dbUser)) { + throw new DbConnectionParamsException('Invalid database parameters!'); + } + $params = [ 'dbname' => $dbName, 'user' => $dbUser, @@ -189,11 +186,13 @@ class Migrate2FASecrets extends Command { return DriverManager::getConnection($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 - */ - private function randomUUID($data = null) { + /** + * 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); assert(strlen($data) == 16); -- GitLab From 0474c80ce444386d8c5f2464fdeeb234d33ffea9 Mon Sep 17 00:00:00 2001 From: Akhil Date: Wed, 12 Apr 2023 19:06:58 +0530 Subject: [PATCH 7/8] Throw exception if user not in SSO database --- lib/Command/Migrate2FASecrets.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/Command/Migrate2FASecrets.php b/lib/Command/Migrate2FASecrets.php index 7a63b67a..49698bef 100644 --- a/lib/Command/Migrate2FASecrets.php +++ b/lib/Command/Migrate2FASecrets.php @@ -123,6 +123,9 @@ class Migrate2FASecrets extends Command { $secret = (string) $row['secret']; $decryptedSecret = $this->crypto->decrypt($secret); $ssoUserId = $this->ssoMapper->getUserId($username, $this->ssoDbConn); + if(empty($ssoUserId)) { + throw new Exception('Does not exist in SSO database'); + } $entry = $this->getSSOSecretEntry($decryptedSecret, $ssoUserId); $this->ssoMapper->insertCredential($entry, $this->ssoDbConn); } catch(\Exception $e) { @@ -139,8 +142,10 @@ class Migrate2FASecrets extends Command { */ private function getSSOSecretEntry(string $secret, string $ssoUserId) : array { + // Create the random UUID from the sso user ID so multiple entries of same credential do not happen + $id = $this->randomUUID(substr($ssoUserId, 0, 16)); $credentialEntry = [ - 'ID' => $this->randomUUID(), + 'ID' => $id, 'USER_ID' => $ssoUserId, 'USER_LABEL' => 'Murena Cloud 2FA', 'TYPE' => 'otp', -- GitLab From d940c8fba4691a2410dbe662e3a5bea663c3d044 Mon Sep 17 00:00:00 2001 From: Akhil Date: Wed, 12 Apr 2023 19:22:15 +0530 Subject: [PATCH 8/8] Add languages --- lib/Command/Migrate2FASecrets.php | 34 +++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/lib/Command/Migrate2FASecrets.php b/lib/Command/Migrate2FASecrets.php index 49698bef..8155d7b1 100644 --- a/lib/Command/Migrate2FASecrets.php +++ b/lib/Command/Migrate2FASecrets.php @@ -11,6 +11,8 @@ use Symfony\Component\Console\Output\OutputInterface; use OCP\Security\ICrypto; use OCP\IDBConnection; use OCP\IUserManager; +use OCP\IUser; +use OCP\IConfig; use OCA\EcloudAccounts\Db\SSOMapper; use OCA\EcloudAccounts\Exception\DbConnectionParamsException; use Doctrine\DBAL\DriverManager; @@ -23,13 +25,22 @@ class Migrate2FASecrets extends Command { private ICrypto $crypto; private IUserManager $userManager; private OutputInterface $commandOutput; + private IConfig $config; private const TOTP_SECRET_TABLE = 'twofactor_totp_secrets'; - - public function __construct(IDBConnection $dbConn, ICrypto $crypto, SSOMapper $ssoMapper, IUserManager $userManager) { + private const USER_LABELS = [ + 'en' => 'Murena Cloud 2FA', + 'es' => 'Murena Cloud 2FA', + 'de' => 'Murena Cloud 2FA', + 'it' => 'Murena Cloud 2FA', + 'fr' => 'Murena Cloud 2FA', + ]; + + public function __construct(IDBConnection $dbConn, ICrypto $crypto, SSOMapper $ssoMapper, IUserManager $userManager, IConfig $config) { $this->ssoMapper = $ssoMapper; $this->userManager = $userManager; $this->dbConn = $dbConn; $this->crypto = $crypto; + $this->config = $config; parent::__construct(); } @@ -120,13 +131,22 @@ class Migrate2FASecrets extends Command { while ($row = $result->fetch()) { try { $username = (string) $row['user_id']; + if (!$this->userManager->get($username) instanceof IUser) { + throw new \Exception('No user found in nextcloud with given username'); + } + $secret = (string) $row['secret']; $decryptedSecret = $this->crypto->decrypt($secret); $ssoUserId = $this->ssoMapper->getUserId($username, $this->ssoDbConn); - if(empty($ssoUserId)) { - throw new Exception('Does not exist in SSO database'); + if (empty($ssoUserId)) { + throw new \Exception('Does not exist in SSO database'); } - $entry = $this->getSSOSecretEntry($decryptedSecret, $ssoUserId); + + $language = $this->config->getUserValue($uid, 'core', 'lang', 'en'); + if (!array_key_exists($language, self::USER_LABELS)) { + $language = 'en'; + } + $entry = $this->getSSOSecretEntry($decryptedSecret, $ssoUserId, $language); $this->ssoMapper->insertCredential($entry, $this->ssoDbConn); } catch(\Exception $e) { $this->commandOutput->writeln('Error inserting entry for user ' . $username . ' message: ' . $e->getMessage()); @@ -141,9 +161,11 @@ class Migrate2FASecrets extends Command { * @return array */ - private function getSSOSecretEntry(string $secret, string $ssoUserId) : array { + private function getSSOSecretEntry(string $secret, string $ssoUserId, string $language) : array { // Create the random UUID from the sso user ID so multiple entries of same credential do not happen $id = $this->randomUUID(substr($ssoUserId, 0, 16)); + + $userLabel = self::USER_LABELS[$language]; $credentialEntry = [ 'ID' => $id, 'USER_ID' => $ssoUserId, -- GitLab