Loading lib/Command/Migrate2FASecrets.php +128 −34 Original line number Diff line number Diff line Loading @@ -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(); } Loading @@ -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 Loading lib/Db/SSOMapper.php +11 −57 Original line number Diff line number Diff line Loading @@ -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'; Loading @@ -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; } } } } Loading
lib/Command/Migrate2FASecrets.php +128 −34 Original line number Diff line number Diff line Loading @@ -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(); } Loading @@ -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 Loading
lib/Db/SSOMapper.php +11 −57 Original line number Diff line number Diff line Loading @@ -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'; Loading @@ -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; } } } }