From 61f1b0e349f72e68832295bfa6bdca2cb9e15e69 Mon Sep 17 00:00:00 2001 From: Akhil Date: Thu, 29 Jun 2023 23:47:16 +0530 Subject: [PATCH 01/10] Add event listener to sync 2fa state change --- lib/AppInfo/Application.php | 8 +- lib/Command/Migrate2FASecrets.php | 202 ++---------------- lib/Db/SSOMapper.php | 164 +++++++++++++- lib/Db/TwoFactorMapper.php | 50 +++++ .../TwoFactorStateChangedListener.php | 58 +++++ 5 files changed, 291 insertions(+), 191 deletions(-) create mode 100644 lib/Db/TwoFactorMapper.php create mode 100644 lib/Listeners/TwoFactorStateChangedListener.php diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 3574f538..47cc7b6b 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -37,6 +37,9 @@ use OCP\User\Events\UserChangedEvent; use OCA\EcloudAccounts\Listeners\UserChangedListener; use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent; use OCA\EcloudAccounts\Listeners\BeforeTemplateRenderedListener; +use OCA\EcloudAccounts\Listeners\TwoFactorStateChangedListener; +use OCA\TwoFactorTOTP\Event\StateChanged; +use OCP\IUserManager; class Application extends App implements IBootstrap { public const APP_ID = 'ecloud-accounts'; @@ -48,13 +51,16 @@ class Application extends App implements IBootstrap { public function register(IRegistrationContext $context): void { $context->registerEventListener(BeforeUserDeletedEvent::class, BeforeUserDeletedListener::class); $context->registerEventListener(UserChangedEvent::class, UserChangedListener::class); + $context->registerEventListener(StateChanged::class, TwoFactorStateChangedListener::class); // $context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class); } public function boot(IBootContext $context): void { $serverContainer = $context->getServerContainer(); $serverContainer->registerService('LDAPConnectionService', function ($c) { - return new LDAPConnectionService(); + return new LDAPConnectionService( + $c->get(IUserManager::class) + ); }); } } diff --git a/lib/Command/Migrate2FASecrets.php b/lib/Command/Migrate2FASecrets.php index 8155d7b1..48bc8bef 100644 --- a/lib/Command/Migrate2FASecrets.php +++ b/lib/Command/Migrate2FASecrets.php @@ -8,96 +8,36 @@ 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 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; -use Doctrine\DBAL\Connection; +use OCA\EcloudAccounts\Db\TwoFactorMapper; class Migrate2FASecrets extends Command { private SSOMapper $ssoMapper; - private IDBConnection $dbConn; - private Connection $ssoDbConn; - private ICrypto $crypto; - private IUserManager $userManager; + private TwoFactorMapper $twoFactorMapper; private OutputInterface $commandOutput; - private IConfig $config; - private const TOTP_SECRET_TABLE = 'twofactor_totp_secrets'; - 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) { + public function __construct(SSOMapper $ssoMapper, TwoFactorMapper $twoFactorMapper) { $this->ssoMapper = $ssoMapper; - $this->userManager = $userManager; - $this->dbConn = $dbConn; - $this->crypto = $crypto; - $this->config = $config; + $this->twoFactorMapper = $twoFactorMapper; parent::__construct(); } protected function configure(): void { $this - ->setName('ecloud-accounts:migrate-2fa-secrets') - ->setDescription('Migrates 2FA secrets to SSO database') - ->addOption( - '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 - ); + ->setName('ecloud-accounts:migrate-2fa-secrets') + ->setDescription('Migrates 2FA secrets to SSO database') + ->addOption( + 'users', + null, + InputOption::VALUE_OPTIONAL, + 'comma separated list of users', + '' + ); } 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'); - $this->ssoDbConn = $this->getDatabaseConnection($dbName, $dbHost, $dbPort, $dbPassword, $dbUser); - $usernames = []; $usernameList = $input->getOption('users'); if (!empty($usernameList)) { @@ -117,119 +57,15 @@ class Migrate2FASecrets extends Command { * @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()) { + $entries = $this->twoFactorMapper->getEntries($usernames); + foreach($entries as $entry) { 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'); - } - - $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()); + $this->ssoMapper->migrateSecret($entry['username'], $entry['secret']); + } + catch(\Exception $e) { + $this->commandOutput->writeln('Error inserting entry for user ' . $entry['username'] . ' message: ' . $e->getMessage()); continue; } } } - - /** - * Create secret entry compatible with Keycloak schema - * - * @return 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, - 'USER_LABEL' => 'Murena Cloud 2FA', - 'TYPE' => 'otp', - 'SECRET_DATA' => json_encode([ - 'value' => $secret - ]), - 'CREDENTIAL_DATA' => json_encode([ - 'subType' => 'nextcloud_totp', - 'period' => 30, - 'digits' => 6, - 'algorithm' => 'HmacSHA1', - ]), - ]; - - foreach ($credentialEntry as $key => &$value) { - $value = "'" . $value . "'"; - } - $credentialEntry['CREATED_DATE'] = round(microtime(true) * 1000); - $credentialEntry['PRIORITY'] = 10; - - return $credentialEntry; - } - - /** - * 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, - 'password' => $dbPassword, - 'host' => $dbHost, - 'port' => $dbPort, - 'driver' => 'pdo_mysql' - ]; - - 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 - * - * @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); - - // 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 index 1e711eda..8f024b8a 100644 --- a/lib/Db/SSOMapper.php +++ b/lib/Db/SSOMapper.php @@ -4,21 +4,42 @@ namespace OCA\EcloudAccounts\Db; use OCP\IConfig; use OCP\ILogger; +use Doctrine\DBAL\DriverManager; use Doctrine\DBAL\Connection; +use OCP\IUserManager; +use OCP\Security\ICrypto; +use OCP\IUser; + class SSOMapper { - private $config; - private $logger; + private IConfig $config; + private ILogger $logger; + private Connection $conn; + private IUserManager $userManager; + private ICrypto $crypto; + private const USER_ATTRIBUTE_TABLE = 'USER_ATTRIBUTE'; private const CREDENTIAL_TABLE = 'CREDENTIAL'; - public function __construct(IConfig $config, ILogger $logger) { + private const SSO_CONFIG_KEY = 'keycloak'; + 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(IConfig $config, IUserManager $userManager, ILogger $logger, ICrypto $crypto) { $this->config = $config; $this->logger = $logger; + $this->userManager = $userManager; + $this->crypto = $crypto; + $this->initConnection(); } - public function getUserId(string $username, Connection $conn) : string { - $qb = $conn->createQueryBuilder(); + public function getUserId(string $username) : string { + $qb = $this->conn->createQueryBuilder(); $qb->select('USER_ID') ->from(self::USER_ATTRIBUTE_TABLE) ->where('NAME = "LDAP_ID"') @@ -29,10 +50,139 @@ class SSOMapper { return (string) $result->fetchOne(); } - public function insertCredential(array $entry, Connection $conn) { - $qb = $conn->createQueryBuilder(); + public function deleteSecret(string $username) { + $qb = $this->conn->createQueryBuilder(); + $qb->delete(self::CREDENTIAL_TABLE) + ->where('USER_ID = :username') + ->andWhere('CREDENTIAL_DATA LIKE %nextcloud_totp%') + ->setParameter('username', $username); + } + + public function migrateSecret(string $username, string $secret) { + if (!$this->userManager->get($username) instanceof IUser) { + throw new \Exception('No user found in nextcloud with given username'); + } + + $decryptedSecret = $this->crypto->decrypt($secret); + $ssoUserId = $this->getUserId($username, $this->conn); + if (empty($ssoUserId)) { + throw new \Exception('Does not exist in SSO database'); + } + + $language = $this->config->getUserValue($username, 'core', 'lang', 'en'); + if (!array_key_exists($language, self::USER_LABELS)) { + $language = 'en'; + } + + $entry = $this->getCredentialEntry($decryptedSecret, $ssoUserId, $language); + $this->insertCredential($entry); + + } + + public function insertCredential(array $entry) : void { + $qb = $this->conn->createQueryBuilder(); $qb->insert(self::CREDENTIAL_TABLE) ->values($entry) ->execute(); } + + /** + * Create secret entry compatible with Keycloak schema + * + * @return array + */ + + private function getCredentialEntry(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, + 'USER_LABEL' => 'Murena Cloud 2FA', + 'TYPE' => 'otp', + 'SECRET_DATA' => json_encode([ + 'value' => $secret + ]), + 'CREDENTIAL_DATA' => json_encode([ + 'subType' => 'nextcloud_totp', + 'period' => 30, + 'digits' => 6, + 'algorithm' => 'HmacSHA1', + ]), + ]; + + foreach ($credentialEntry as $key => &$value) { + $value = "'" . $value . "'"; + } + $credentialEntry['CREATED_DATE'] = round(microtime(true) * 1000); + $credentialEntry['PRIORITY'] = 10; + + return $credentialEntry; + } + + private function initConnection() : void { + try { + $params = $this->getConnectionParams(); + $this->conn = DriverManager::getConnection($params); + } catch (Throwable $e) { + $this->logger->error('Error connecting to Keycloak database: ' . $e->getMessage()); + } + } + + 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']) ; + } + + private function getConnectionParams() : array { + $config = $this->config->getSystemValue(self::SSO_CONFIG_KEY); + + if (!$this->isDbConfigValid($config)) { + throw new DbConnectionParamsException('Invalid SQL raw 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; + } + + /** + * 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); + + // 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/TwoFactorMapper.php b/lib/Db/TwoFactorMapper.php new file mode 100644 index 00000000..1148adde --- /dev/null +++ b/lib/Db/TwoFactorMapper.php @@ -0,0 +1,50 @@ +conn = $conn; + } + + public function getEntries(array $usernames = []) : array { + $entries = []; + $qb = $this->conn->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()) { + $entry = [ + 'username' => (string) $row['user_id'], + 'secret' => (string) $row['secret'] + ]; + $entries[] = $entry; + } + return $entries; + } + + public function getSecret(string $username) : string { + $qb = $this->conn->getQueryBuilder(); + $qb->select('secret') + ->from(self::TOTP_SECRET_TABLE) + ->where('user_id = :username') + ->setParameter('username', $username); + $result = $qb->execute(); + + return (string) $result->fetchOne(); + } +} diff --git a/lib/Listeners/TwoFactorStateChangedListener.php b/lib/Listeners/TwoFactorStateChangedListener.php new file mode 100644 index 00000000..7ec18c48 --- /dev/null +++ b/lib/Listeners/TwoFactorStateChangedListener.php @@ -0,0 +1,58 @@ +appManager = $appManager; + $this->ssoMapper = $ssoMapper; + $this->twoFactorMapper = $twoFactorMapper; + $this->logger = $logger; + } + + + public function handle(Event $event): void { + if (!($event instanceof StateChanged) || !$this->appManager->isEnabledForUser(self::TWOFACTOR_APP_ID)) { + return; + } + + $user = $event->getUser(); + $username = $user->getUID(); + try { + // Delete old secret as 2FA secret state has changed + $this->ssoMapper->deleteSecret($username); + + // When state change event is fired by user disabling 2FA, just return + if (!$event->isEnabled()) { + return; + } + + $secret = $this->twoFactorMapper->getSecret($username); + $this->ssoMapper->migrateSecret($username, $secret); + } + catch (Exception $e) { + $stateText = $event->isEnabled() ? 'new secret enabled' : 'disabled'; + $this->logger->error('Error updating secret state(' . $stateText .') for user: ' . $username . ': ' . $e->getMessage()); + } + } + +} -- GitLab From 1e4ef096b2cdea7d590823331ca87ff9a3f3e951 Mon Sep 17 00:00:00 2001 From: Akhil Date: Fri, 30 Jun 2023 15:38:20 +0530 Subject: [PATCH 02/10] php lint fix --- lib/Command/Migrate2FASecrets.php | 5 ++--- lib/Db/SSOMapper.php | 14 +++++--------- lib/Db/TwoFactorMapper.php | 2 -- lib/Listeners/TwoFactorStateChangedListener.php | 6 ++---- 4 files changed, 9 insertions(+), 18 deletions(-) diff --git a/lib/Command/Migrate2FASecrets.php b/lib/Command/Migrate2FASecrets.php index 48bc8bef..ff66108c 100644 --- a/lib/Command/Migrate2FASecrets.php +++ b/lib/Command/Migrate2FASecrets.php @@ -58,11 +58,10 @@ class Migrate2FASecrets extends Command { */ private function migrateUsers(array $usernames = []) : void { $entries = $this->twoFactorMapper->getEntries($usernames); - foreach($entries as $entry) { + foreach ($entries as $entry) { try { $this->ssoMapper->migrateSecret($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/Db/SSOMapper.php b/lib/Db/SSOMapper.php index 8f024b8a..c5ca7d8d 100644 --- a/lib/Db/SSOMapper.php +++ b/lib/Db/SSOMapper.php @@ -10,7 +10,6 @@ use OCP\IUserManager; use OCP\Security\ICrypto; use OCP\IUser; - class SSOMapper { private IConfig $config; private ILogger $logger; @@ -76,7 +75,6 @@ class SSOMapper { $entry = $this->getCredentialEntry($decryptedSecret, $ssoUserId, $language); $this->insertCredential($entry); - } public function insertCredential(array $entry) : void { @@ -165,11 +163,11 @@ class SSOMapper { } /** - * 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); @@ -183,6 +181,4 @@ class SSOMapper { // 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/TwoFactorMapper.php b/lib/Db/TwoFactorMapper.php index 1148adde..5b9739ab 100644 --- a/lib/Db/TwoFactorMapper.php +++ b/lib/Db/TwoFactorMapper.php @@ -2,11 +2,9 @@ namespace OCA\EcloudAccounts\Db; - use OCP\IDBConnection; class TwoFactorMapper { - private IDBConnection $conn; private const TOTP_SECRET_TABLE = 'twofactor_totp_secrets'; diff --git a/lib/Listeners/TwoFactorStateChangedListener.php b/lib/Listeners/TwoFactorStateChangedListener.php index 7ec18c48..72250d4a 100644 --- a/lib/Listeners/TwoFactorStateChangedListener.php +++ b/lib/Listeners/TwoFactorStateChangedListener.php @@ -27,7 +27,7 @@ class TwoFactorStateChangedListener implements IEventListener { $this->ssoMapper = $ssoMapper; $this->twoFactorMapper = $twoFactorMapper; $this->logger = $logger; - } + } public function handle(Event $event): void { @@ -48,11 +48,9 @@ class TwoFactorStateChangedListener implements IEventListener { $secret = $this->twoFactorMapper->getSecret($username); $this->ssoMapper->migrateSecret($username, $secret); - } - catch (Exception $e) { + } catch (Exception $e) { $stateText = $event->isEnabled() ? 'new secret enabled' : 'disabled'; $this->logger->error('Error updating secret state(' . $stateText .') for user: ' . $username . ': ' . $e->getMessage()); } } - } -- GitLab From 505157ae71fbdcaa0131161dacebb54de3f0f1c2 Mon Sep 17 00:00:00 2001 From: Akhil Date: Fri, 30 Jun 2023 15:41:28 +0530 Subject: [PATCH 03/10] fix getUserId call --- lib/Db/SSOMapper.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Db/SSOMapper.php b/lib/Db/SSOMapper.php index c5ca7d8d..88b734b4 100644 --- a/lib/Db/SSOMapper.php +++ b/lib/Db/SSOMapper.php @@ -63,7 +63,7 @@ class SSOMapper { } $decryptedSecret = $this->crypto->decrypt($secret); - $ssoUserId = $this->getUserId($username, $this->conn); + $ssoUserId = $this->getUserId($username); if (empty($ssoUserId)) { throw new \Exception('Does not exist in SSO database'); } @@ -98,7 +98,7 @@ class SSOMapper { $credentialEntry = [ 'ID' => $id, 'USER_ID' => $ssoUserId, - 'USER_LABEL' => 'Murena Cloud 2FA', + 'USER_LABEL' => $userLabel, 'TYPE' => 'otp', 'SECRET_DATA' => json_encode([ 'value' => $secret -- GitLab From a0733e114ff72269665d36c22a6ebe452b2ce092 Mon Sep 17 00:00:00 2001 From: Akhil Date: Fri, 30 Jun 2023 16:42:26 +0530 Subject: [PATCH 04/10] Temporarily update gitlab ci file --- .gitlab-ci.yml | 108 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 100 insertions(+), 8 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6119917f..28e4e402 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,9 +1,101 @@ +#variables: +# TO_PACKAGE: 'appinfo l10n lib templates js img' +#include: +# - project: "e/infra/ecloud/nextcloud-apps/ci-templates" +# ref: main +# file: "nc-apps-lint-build-frontend.yml" +# - project: "e/infra/ecloud/nextcloud-apps/ci-templates" +# ref: main +# file: "nc-apps-deploy.yml" +#include: +# - project: "e/infra/ecloud/nextcloud-apps/ci-templates" +# ref: main +# file: "nc-apps-lint-build-frontend.yml" +# - project: "e/infra/ecloud/nextcloud-apps/ci-templates" +# ref: main +# file: "nc-apps-deploy.yml" + variables: - TO_PACKAGE: 'appinfo l10n lib templates js img' -include: - - project: "e/infra/ecloud/nextcloud-apps/ci-templates" - ref: main - file: "nc-apps-lint-build-frontend.yml" - - project: "e/infra/ecloud/nextcloud-apps/ci-templates" - ref: main - file: "nc-apps-deploy.yml" + APP_NAME: $CI_PROJECT_NAME + TO_PACKAGE: 'appinfo js css l10n lib img templates' + CONTAINER_IMAGE: ubuntu + CONTAINER_TAG: focal + CONTAINER_NAME: nextcloud + APP_ENABLE_ARGS: '' + +.frontend:base: + image: node:15.14.0-stretch + before_script: + - npm set cache .npm + - npm install --prefer-offline --no-audit + cache: + key: ${CI_COMMIT_REF_SLUG} + paths: + - .npm/ + - node_modules/ + +install-node-deps: + extends: .frontend:base + stage: .pre + before_script: + - node --version + - npm --version + script: + - npm ci --cache .npm --prefer-offline + only: + changes: + - package*.json + +build-frontend: + extends: .frontend:base + stage: build + script: + - npm run build + - mkdir -p dist/${APP_NAME} + - rm -f dist/js/*.map + - echo "packaging ${TO_PACKAGE}" && cp -a ${TO_PACKAGE} dist/${APP_NAME} && rm -rf dist/js + - find dist/${APP_NAME} -type d -exec chmod 755 {} \; + - find dist/${APP_NAME} -type f -exec chmod 644 {} \; + artifacts: + paths: + - dist/ + +.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 + - tags + - dev/checkbox-issue-drop-account + environment: + name: staging/01 + url: https://eeo.one -- GitLab From b8f2a9170501c5cb105fe4a5658b9aa596a9f4e1 Mon Sep 17 00:00:00 2001 From: Akhil Date: Fri, 30 Jun 2023 16:46:47 +0530 Subject: [PATCH 05/10] Temporarily update gitlab ci file --- .gitlab-ci.yml | 67 ++++++++------------------------------------------ 1 file changed, 10 insertions(+), 57 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 28e4e402..b6233d12 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,64 +1,17 @@ -#variables: -# TO_PACKAGE: 'appinfo l10n lib templates js img' -#include: -# - project: "e/infra/ecloud/nextcloud-apps/ci-templates" -# ref: main -# file: "nc-apps-lint-build-frontend.yml" -# - project: "e/infra/ecloud/nextcloud-apps/ci-templates" -# ref: main -# file: "nc-apps-deploy.yml" -#include: -# - project: "e/infra/ecloud/nextcloud-apps/ci-templates" -# ref: main -# file: "nc-apps-lint-build-frontend.yml" -# - project: "e/infra/ecloud/nextcloud-apps/ci-templates" -# ref: main -# file: "nc-apps-deploy.yml" - variables: - APP_NAME: $CI_PROJECT_NAME - TO_PACKAGE: 'appinfo js css l10n lib img templates' CONTAINER_IMAGE: ubuntu CONTAINER_TAG: focal CONTAINER_NAME: nextcloud + APP_NAME: $CI_PROJECT_NAME APP_ENABLE_ARGS: '' - -.frontend:base: - image: node:15.14.0-stretch - before_script: - - npm set cache .npm - - npm install --prefer-offline --no-audit - cache: - key: ${CI_COMMIT_REF_SLUG} - paths: - - .npm/ - - node_modules/ - -install-node-deps: - extends: .frontend:base - stage: .pre - before_script: - - node --version - - npm --version - script: - - npm ci --cache .npm --prefer-offline - only: - changes: - - package*.json - -build-frontend: - extends: .frontend:base - stage: build - script: - - npm run build - - mkdir -p dist/${APP_NAME} - - rm -f dist/js/*.map - - echo "packaging ${TO_PACKAGE}" && cp -a ${TO_PACKAGE} dist/${APP_NAME} && rm -rf dist/js - - find dist/${APP_NAME} -type d -exec chmod 755 {} \; - - find dist/${APP_NAME} -type f -exec chmod 644 {} \; - artifacts: - paths: - - dist/ + TO_PACKAGE: 'appinfo l10n lib templates js img' +include: + - project: "e/infra/ecloud/nextcloud-apps/ci-templates" + ref: main + file: "nc-apps-lint-build-frontend.yml" + - project: "e/infra/ecloud/nextcloud-apps/ci-templates" + ref: main + file: "nc-apps-deploy.yml" .deploy:nextcloud-app: stage: deploy @@ -94,8 +47,8 @@ deploy:staging: - main - murena-main - production + - dev/sync-new-secrets - tags - - dev/checkbox-issue-drop-account environment: name: staging/01 url: https://eeo.one -- GitLab From 8b25757186a22bd06df6575ad16bc86405f7c1b7 Mon Sep 17 00:00:00 2001 From: Akhil Date: Fri, 30 Jun 2023 17:16:21 +0530 Subject: [PATCH 06/10] Fix syntax error in delete query --- lib/Db/SSOMapper.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/Db/SSOMapper.php b/lib/Db/SSOMapper.php index 88b734b4..89a265b3 100644 --- a/lib/Db/SSOMapper.php +++ b/lib/Db/SSOMapper.php @@ -9,6 +9,7 @@ use Doctrine\DBAL\Connection; use OCP\IUserManager; use OCP\Security\ICrypto; use OCP\IUser; +use OCA\EcloudAccounts\Exception\DbConnectionParamsException; class SSOMapper { private IConfig $config; @@ -50,11 +51,13 @@ class SSOMapper { } public function deleteSecret(string $username) { + $userId = $this->getUserId($username); $qb = $this->conn->createQueryBuilder(); $qb->delete(self::CREDENTIAL_TABLE) ->where('USER_ID = :username') - ->andWhere('CREDENTIAL_DATA LIKE %nextcloud_totp%') - ->setParameter('username', $username); + ->andWhere('CREDENTIAL_DATA LIKE "%nextcloud_totp%"') + ->setParameter('username', $userId) + ->execute(); } public function migrateSecret(string $username, string $secret) { @@ -148,7 +151,7 @@ class SSOMapper { $config = $this->config->getSystemValue(self::SSO_CONFIG_KEY); if (!$this->isDbConfigValid($config)) { - throw new DbConnectionParamsException('Invalid SQL raw configuration!'); + throw new DbConnectionParamsException('Invalid SSO database configuration!'); } $params = [ -- GitLab From 030a346a71990bff2239d62a996c9238fffb2b5f Mon Sep 17 00:00:00 2001 From: Akhil Date: Fri, 30 Jun 2023 17:19:12 +0530 Subject: [PATCH 07/10] Delete in migrate --- lib/Command/Migrate2FASecrets.php | 2 +- lib/Db/SSOMapper.php | 7 +++++-- lib/Listeners/TwoFactorStateChangedListener.php | 10 ++++------ 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/Command/Migrate2FASecrets.php b/lib/Command/Migrate2FASecrets.php index ff66108c..13bddf36 100644 --- a/lib/Command/Migrate2FASecrets.php +++ b/lib/Command/Migrate2FASecrets.php @@ -60,7 +60,7 @@ class Migrate2FASecrets extends Command { $entries = $this->twoFactorMapper->getEntries($usernames); foreach ($entries as $entry) { try { - $this->ssoMapper->migrateSecret($entry['username'], $entry['secret']); + $this->ssoMapper->migrateCredential($entry['username'], $entry['secret']); } catch(\Exception $e) { $this->commandOutput->writeln('Error inserting entry for user ' . $entry['username'] . ' message: ' . $e->getMessage()); continue; diff --git a/lib/Db/SSOMapper.php b/lib/Db/SSOMapper.php index 89a265b3..947ecce1 100644 --- a/lib/Db/SSOMapper.php +++ b/lib/Db/SSOMapper.php @@ -50,7 +50,7 @@ class SSOMapper { return (string) $result->fetchOne(); } - public function deleteSecret(string $username) { + public function deleteCredential(string $username) { $userId = $this->getUserId($username); $qb = $this->conn->createQueryBuilder(); $qb->delete(self::CREDENTIAL_TABLE) @@ -60,7 +60,7 @@ class SSOMapper { ->execute(); } - public function migrateSecret(string $username, string $secret) { + public function migrateCredential(string $username, string $secret) { if (!$this->userManager->get($username) instanceof IUser) { throw new \Exception('No user found in nextcloud with given username'); } @@ -76,6 +76,9 @@ class SSOMapper { $language = 'en'; } + // Only one "nextcloud_totp" at a time + $this->deleteCredential($username); + $entry = $this->getCredentialEntry($decryptedSecret, $ssoUserId, $language); $this->insertCredential($entry); } diff --git a/lib/Listeners/TwoFactorStateChangedListener.php b/lib/Listeners/TwoFactorStateChangedListener.php index 72250d4a..f728ec4b 100644 --- a/lib/Listeners/TwoFactorStateChangedListener.php +++ b/lib/Listeners/TwoFactorStateChangedListener.php @@ -37,17 +37,15 @@ class TwoFactorStateChangedListener implements IEventListener { $user = $event->getUser(); $username = $user->getUID(); - try { - // Delete old secret as 2FA secret state has changed - $this->ssoMapper->deleteSecret($username); - - // When state change event is fired by user disabling 2FA, just return + try { + // When state change event is fired by user disabling 2FA, delete existing credential and return if (!$event->isEnabled()) { + $this->ssoMapper->deleteCredential($username); return; } $secret = $this->twoFactorMapper->getSecret($username); - $this->ssoMapper->migrateSecret($username, $secret); + $this->ssoMapper->migrateCredential($username, $secret); } catch (Exception $e) { $stateText = $event->isEnabled() ? 'new secret enabled' : 'disabled'; $this->logger->error('Error updating secret state(' . $stateText .') for user: ' . $username . ': ' . $e->getMessage()); -- GitLab From 9faa696e7bc19d8fd99194eb1e006b88cd31d459 Mon Sep 17 00:00:00 2001 From: Akhil Date: Fri, 30 Jun 2023 17:20:14 +0530 Subject: [PATCH 08/10] Lint php --- lib/Listeners/TwoFactorStateChangedListener.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Listeners/TwoFactorStateChangedListener.php b/lib/Listeners/TwoFactorStateChangedListener.php index f728ec4b..860ad4b6 100644 --- a/lib/Listeners/TwoFactorStateChangedListener.php +++ b/lib/Listeners/TwoFactorStateChangedListener.php @@ -37,7 +37,7 @@ class TwoFactorStateChangedListener implements IEventListener { $user = $event->getUser(); $username = $user->getUID(); - try { + try { // When state change event is fired by user disabling 2FA, delete existing credential and return if (!$event->isEnabled()) { $this->ssoMapper->deleteCredential($username); -- GitLab From 5b1293f9fa13718b5fe6e5aa6494080867b4678e Mon Sep 17 00:00:00 2001 From: Akhil Date: Fri, 30 Jun 2023 17:30:07 +0530 Subject: [PATCH 09/10] add subType in like query --- lib/Db/SSOMapper.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/Db/SSOMapper.php b/lib/Db/SSOMapper.php index 947ecce1..49b91e4e 100644 --- a/lib/Db/SSOMapper.php +++ b/lib/Db/SSOMapper.php @@ -55,7 +55,8 @@ class SSOMapper { $qb = $this->conn->createQueryBuilder(); $qb->delete(self::CREDENTIAL_TABLE) ->where('USER_ID = :username') - ->andWhere('CREDENTIAL_DATA LIKE "%nextcloud_totp%"') + ->andWhere('TYPE = "otp"') + ->andWhere($qb->expr()->like('CREDENTIAL_DATA', '%"subType":"nextcloud_totp"%')) ->setParameter('username', $userId) ->execute(); } -- GitLab From 804690ec89cd14b0cb57b9247688e2f8733574bd Mon Sep 17 00:00:00 2001 From: Akhil Date: Fri, 30 Jun 2023 17:35:51 +0530 Subject: [PATCH 10/10] Fix subType like query --- lib/Db/SSOMapper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Db/SSOMapper.php b/lib/Db/SSOMapper.php index 49b91e4e..d172ddda 100644 --- a/lib/Db/SSOMapper.php +++ b/lib/Db/SSOMapper.php @@ -56,7 +56,7 @@ class SSOMapper { $qb->delete(self::CREDENTIAL_TABLE) ->where('USER_ID = :username') ->andWhere('TYPE = "otp"') - ->andWhere($qb->expr()->like('CREDENTIAL_DATA', '%"subType":"nextcloud_totp"%')) + ->andWhere('CREDENTIAL_DATA LIKE "%\"subType\":\"nextcloud_totp\"%"') ->setParameter('username', $userId) ->execute(); } -- GitLab