Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(oauth2): adjust db schemas when migrating from owncloud #50193

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
3 changes: 1 addition & 2 deletions lib/private/Repair.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
*/
namespace OC;

use OC\DB\Connection;
use OC\DB\ConnectionAdapter;
use OC\Repair\AddAppConfigLazyMigration;
use OC\Repair\AddBruteForceCleanupJob;
Expand Down Expand Up @@ -167,7 +166,7 @@ public static function getRepairSteps(): array {
\OC::$server->getUserManager(),
\OC::$server->getConfig()
),
new MigrateOauthTables(\OC::$server->get(Connection::class)),
\OC::$server->get(MigrateOauthTables::class),
new UpdateLanguageCodes(\OC::$server->getDatabaseConnection(), \OC::$server->getConfig()),
new AddLogRotateJob(\OC::$server->getJobList()),
new ClearFrontendCaches(\OC::$server->getMemCacheFactory(), \OCP\Server::get(JSCombiner::class)),
Expand Down
99 changes: 95 additions & 4 deletions lib/private/Repair/Owncloud/MigrateOauthTables.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,18 @@
*/
namespace OC\Repair\Owncloud;

use OC\Authentication\Token\IProvider as ITokenProvider;
use OC\DB\Connection;
use OC\DB\SchemaWrapper;
use OCA\OAuth2\Db\AccessToken;
use OCA\OAuth2\Db\AccessTokenMapper;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Authentication\Token\IToken;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Migration\IOutput;
use OCP\Migration\IRepairStep;
use OCP\Security\ICrypto;
use OCP\Security\ISecureRandom;

class MigrateOauthTables implements IRepairStep {
/** @var Connection */
Expand All @@ -18,7 +25,14 @@ class MigrateOauthTables implements IRepairStep {
/**
* @param Connection $db
*/
public function __construct(Connection $db) {
public function __construct(
Connection $db,
private AccessTokenMapper $accessTokenMapper,
private ITokenProvider $tokenProvider,
private ISecureRandom $random,
private ITimeFactory $timeFactory,
private ICrypto $crypto,
) {
$this->db = $db;
}

Expand All @@ -36,26 +50,55 @@ public function run(IOutput $output) {
return;
}

$output->info('Update the oauth2_access_tokens table schema.');
// Create column and then migrate before handling unique index.
// So that we can distinguish between legacy (from oc) and new rows (from nc).
$table = $schema->getTable('oauth2_access_tokens');
if (!$table->hasColumn('hashed_code')) {
$output->info('Prepare the oauth2_access_tokens table schema.');
$table->addColumn('hashed_code', 'string', [
'notnull' => true,
'length' => 128,
]);

// Regenerate schema after migrating to it
$this->db->migrateToSchema($schema->getWrappedSchema());
$schema = new SchemaWrapper($this->db);
st3iny marked this conversation as resolved.
Show resolved Hide resolved
}

$output->info('Update the oauth2_access_tokens table schema.');
$table = $schema->getTable('oauth2_access_tokens');
if (!$table->hasColumn('encrypted_token')) {
$table->addColumn('encrypted_token', 'string', [
'notnull' => true,
'length' => 786,
]);
}
if (!$table->hasIndex('oauth2_access_hash_idx')) {
// Drop legacy access codes first to prevent integrity constraint violations
$qb = $this->db->getQueryBuilder();
$qb->delete('oauth2_access_tokens')
->where($qb->expr()->eq('hashed_code', $qb->createNamedParameter('')));
$qb->executeStatement();

$table->addUniqueIndex(['hashed_code'], 'oauth2_access_hash_idx');
}
if (!$table->hasIndex('oauth2_access_client_id_idx')) {
$table->addIndex(['client_id'], 'oauth2_access_client_id_idx');
}
if (!$table->hasColumn('token_id')) {
$table->addColumn('token_id', 'integer', [
'notnull' => true,
]);
}
if ($table->hasColumn('expires')) {
$table->dropColumn('expires');
}
if ($table->hasColumn('user_id')) {
$table->dropColumn('user_id');
}
if ($table->hasColumn('token')) {
$table->dropColumn('token');
}

$output->info('Update the oauth2_clients table schema.');
$table = $schema->getTable('oauth2_clients');
Expand Down Expand Up @@ -99,10 +142,10 @@ public function run(IOutput $output) {
$table->addIndex(['client_identifier'], 'oauth2_client_id_idx');
}

$this->db->migrateToSchema($schema->getWrappedSchema());

// Regenerate schema after migrating to it
$this->db->migrateToSchema($schema->getWrappedSchema());
$schema = new SchemaWrapper($this->db);

if ($schema->getTable('oauth2_clients')->hasColumn('identifier')) {
$output->info("Move identifier column's data to the new client_identifier column.");
// 1. Fetch all [id, identifier] couple.
Expand All @@ -124,7 +167,10 @@ public function run(IOutput $output) {
$output->info('Drop the identifier column.');
$table = $schema->getTable('oauth2_clients');
$table->dropColumn('identifier');

// Regenerate schema after migrating to it
$this->db->migrateToSchema($schema->getWrappedSchema());
$schema = new SchemaWrapper($this->db);
}

$output->info('Delete clients (and their related access tokens) with the redirect_uri starting with oc:// or ending with *');
Expand Down Expand Up @@ -157,5 +203,50 @@ public function run(IOutput $output) {
$qbDeleteClients->expr()->iLike('redirect_uri', $qbDeleteClients->createNamedParameter('%*', IQueryBuilder::PARAM_STR))
);
$qbDeleteClients->executeStatement();

// Migrate legacy refresh tokens from oc
if ($schema->hasTable('oauth2_refresh_tokens')) {
$output->info('Migrate legacy oauth2 refresh tokens.');

$qbSelect = $this->db->getQueryBuilder();
$qbSelect->select('*')
->from('oauth2_refresh_tokens');
$result = $qbSelect->executeQuery();
$now = $this->timeFactory->now()->getTimestamp();
$index = 0;
while ($row = $result->fetch()) {
$clientId = $row['client_id'];
$refreshToken = $row['token'];

// Insert expired token so that it can be rotated on the next refresh
$accessToken = $this->random->generate(72, ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS);
$authToken = $this->tokenProvider->generateToken(
$accessToken,
$row['user_id'],
$row['user_id'],
null,
"oc_migrated_client${clientId}_t{$now}_i$index",
IToken::PERMANENT_TOKEN,
IToken::DO_NOT_REMEMBER,
);
$authToken->setExpires($now - 3600);
$this->tokenProvider->updateToken($authToken);

$accessTokenEntity = new AccessToken();
$accessTokenEntity->setTokenId($authToken->getId());
$accessTokenEntity->setClientId($clientId);
$accessTokenEntity->setHashedCode(hash('sha512', $refreshToken));
$accessTokenEntity->setEncryptedToken($this->crypto->encrypt($accessToken, $refreshToken));
$accessTokenEntity->setCodeCreatedAt($now);
$accessTokenEntity->setTokenCount(1);
$this->accessTokenMapper->insert($accessTokenEntity);

$index++;
}
$result->closeCursor();

$schema->dropTable('oauth2_refresh_tokens');
$schema->performDropTableCalls();
}
}
}
Loading