Browse Source

Require a token for "Edit locally"

Signed-off-by: Joas Schilling <coding@schilljs.com>
tags/v26.0.0beta1
Joas Schilling 1 year ago
parent
commit
cfbbace450
No account linked to committer's email address

+ 1
- 1
apps/files/appinfo/info.xml View File

@@ -5,7 +5,7 @@
<name>Files</name>
<summary>File Management</summary>
<description>File Management</description>
<version>1.21.0</version>
<version>1.21.1</version>
<licence>agpl</licence>
<author>Robin Appelman</author>
<author>Vincent Petry</author>

+ 14
- 0
apps/files/appinfo/routes.php View File

@@ -37,6 +37,8 @@ declare(strict_types=1);
*/
namespace OCA\Files\AppInfo;

use OCA\Files\Controller\OpenLocalEditorController;

/** @var Application $application */
$application = \OC::$server->query(Application::class);
$application->registerRoutes(
@@ -169,6 +171,18 @@ $application->registerRoutes(
'url' => '/api/v1/transferownership/{id}',
'verb' => 'DELETE',
],
[
/** @see OpenLocalEditorController::create() */
'name' => 'OpenLocalEditor#create',
'url' => '/api/v1/openlocaleditor',
'verb' => 'POST',
],
[
/** @see OpenLocalEditorController::validate() */
'name' => 'OpenLocalEditor#validate',
'url' => '/api/v1/openlocaleditor/{token}',
'verb' => 'POST',
],
],
]
);

+ 4
- 0
apps/files/composer/composer/autoload_classmap.php View File

@@ -35,9 +35,12 @@ return array(
'OCA\\Files\\Controller\\ApiController' => $baseDir . '/../lib/Controller/ApiController.php',
'OCA\\Files\\Controller\\DirectEditingController' => $baseDir . '/../lib/Controller/DirectEditingController.php',
'OCA\\Files\\Controller\\DirectEditingViewController' => $baseDir . '/../lib/Controller/DirectEditingViewController.php',
'OCA\\Files\\Controller\\OpenLocalEditorController' => $baseDir . '/../lib/Controller/OpenLocalEditorController.php',
'OCA\\Files\\Controller\\TemplateController' => $baseDir . '/../lib/Controller/TemplateController.php',
'OCA\\Files\\Controller\\TransferOwnershipController' => $baseDir . '/../lib/Controller/TransferOwnershipController.php',
'OCA\\Files\\Controller\\ViewController' => $baseDir . '/../lib/Controller/ViewController.php',
'OCA\\Files\\Db\\OpenLocalEditor' => $baseDir . '/../lib/Db/OpenLocalEditor.php',
'OCA\\Files\\Db\\OpenLocalEditorMapper' => $baseDir . '/../lib/Db/OpenLocalEditorMapper.php',
'OCA\\Files\\Db\\TransferOwnership' => $baseDir . '/../lib/Db/TransferOwnership.php',
'OCA\\Files\\Db\\TransferOwnershipMapper' => $baseDir . '/../lib/Db/TransferOwnershipMapper.php',
'OCA\\Files\\DirectEditingCapabilities' => $baseDir . '/../lib/DirectEditingCapabilities.php',
@@ -48,6 +51,7 @@ return array(
'OCA\\Files\\Listener\\LegacyLoadAdditionalScriptsAdapter' => $baseDir . '/../lib/Listener/LegacyLoadAdditionalScriptsAdapter.php',
'OCA\\Files\\Listener\\LoadSidebarListener' => $baseDir . '/../lib/Listener/LoadSidebarListener.php',
'OCA\\Files\\Migration\\Version11301Date20191205150729' => $baseDir . '/../lib/Migration/Version11301Date20191205150729.php',
'OCA\\Files\\Migration\\Version12101Date20221011153334' => $baseDir . '/../lib/Migration/Version12101Date20221011153334.php',
'OCA\\Files\\Notification\\Notifier' => $baseDir . '/../lib/Notification/Notifier.php',
'OCA\\Files\\Search\\FilesSearchProvider' => $baseDir . '/../lib/Search/FilesSearchProvider.php',
'OCA\\Files\\Service\\DirectEditingService' => $baseDir . '/../lib/Service/DirectEditingService.php',

+ 4
- 0
apps/files/composer/composer/autoload_static.php View File

@@ -50,9 +50,12 @@ class ComposerStaticInitFiles
'OCA\\Files\\Controller\\ApiController' => __DIR__ . '/..' . '/../lib/Controller/ApiController.php',
'OCA\\Files\\Controller\\DirectEditingController' => __DIR__ . '/..' . '/../lib/Controller/DirectEditingController.php',
'OCA\\Files\\Controller\\DirectEditingViewController' => __DIR__ . '/..' . '/../lib/Controller/DirectEditingViewController.php',
'OCA\\Files\\Controller\\OpenLocalEditorController' => __DIR__ . '/..' . '/../lib/Controller/OpenLocalEditorController.php',
'OCA\\Files\\Controller\\TemplateController' => __DIR__ . '/..' . '/../lib/Controller/TemplateController.php',
'OCA\\Files\\Controller\\TransferOwnershipController' => __DIR__ . '/..' . '/../lib/Controller/TransferOwnershipController.php',
'OCA\\Files\\Controller\\ViewController' => __DIR__ . '/..' . '/../lib/Controller/ViewController.php',
'OCA\\Files\\Db\\OpenLocalEditor' => __DIR__ . '/..' . '/../lib/Db/OpenLocalEditor.php',
'OCA\\Files\\Db\\OpenLocalEditorMapper' => __DIR__ . '/..' . '/../lib/Db/OpenLocalEditorMapper.php',
'OCA\\Files\\Db\\TransferOwnership' => __DIR__ . '/..' . '/../lib/Db/TransferOwnership.php',
'OCA\\Files\\Db\\TransferOwnershipMapper' => __DIR__ . '/..' . '/../lib/Db/TransferOwnershipMapper.php',
'OCA\\Files\\DirectEditingCapabilities' => __DIR__ . '/..' . '/../lib/DirectEditingCapabilities.php',
@@ -63,6 +66,7 @@ class ComposerStaticInitFiles
'OCA\\Files\\Listener\\LegacyLoadAdditionalScriptsAdapter' => __DIR__ . '/..' . '/../lib/Listener/LegacyLoadAdditionalScriptsAdapter.php',
'OCA\\Files\\Listener\\LoadSidebarListener' => __DIR__ . '/..' . '/../lib/Listener/LoadSidebarListener.php',
'OCA\\Files\\Migration\\Version11301Date20191205150729' => __DIR__ . '/..' . '/../lib/Migration/Version11301Date20191205150729.php',
'OCA\\Files\\Migration\\Version12101Date20221011153334' => __DIR__ . '/..' . '/../lib/Migration/Version12101Date20221011153334.php',
'OCA\\Files\\Notification\\Notifier' => __DIR__ . '/..' . '/../lib/Notification/Notifier.php',
'OCA\\Files\\Search\\FilesSearchProvider' => __DIR__ . '/..' . '/../lib/Search/FilesSearchProvider.php',
'OCA\\Files\\Service\\DirectEditingService' => __DIR__ . '/..' . '/../lib/Service/DirectEditingService.php',

+ 16
- 5
apps/files/js/filelist.js View File

@@ -2808,12 +2808,23 @@
},

openLocalClient: function(path) {
var scheme = 'nc://';
var command = 'open';
var uid = OC.getCurrentUser().uid;
var url = scheme + command + '/' + uid + '@' + window.location.host + OC.encodePath(path);
var link = OC.linkToOCS('apps/files/api/v1', 2) + 'openlocaleditor?format=json';

window.location.href = url;
$.post(link, {
path
})
.success(function(result) {
var scheme = 'nc://';
var command = 'open';
var uid = OC.getCurrentUser().uid;
var url = scheme + command + '/' + uid + '@' + window.location.host + OC.encodePath(path);
url += '?token=' + result.ocs.data.token;

window.location.href = url;
})
.fail(function() {
OC.Notification.show(t('files', 'Failed to redirect to client'))
})
},

/**

+ 135
- 0
apps/files/lib/Controller/OpenLocalEditorController.php View File

@@ -0,0 +1,135 @@
<?php

declare(strict_types=1);

/**
* @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

namespace OCA\Files\Controller;

use OCA\Files\Db\OpenLocalEditor;
use OCA\Files\Db\OpenLocalEditorMapper;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\DB\Exception;
use OCP\IRequest;
use OCP\Security\ISecureRandom;
use Psr\Log\LoggerInterface;

class OpenLocalEditorController extends OCSController {
public const TOKEN_LENGTH = 128;
public const TOKEN_DURATION = 600; // 10 Minutes
public const TOKEN_RETRIES = 50;

protected ITimeFactory $timeFactory;
protected OpenLocalEditorMapper $mapper;
protected ISecureRandom $secureRandom;
protected LoggerInterface $logger;
protected ?string $userId;

public function __construct(
string $appName,
IRequest $request,
ITimeFactory $timeFactory,
OpenLocalEditorMapper $mapper,
ISecureRandom $secureRandom,
LoggerInterface $logger,
?string $userId
) {
parent::__construct($appName, $request);

$this->timeFactory = $timeFactory;
$this->mapper = $mapper;
$this->secureRandom = $secureRandom;
$this->logger = $logger;
$this->userId = $userId;
}

/**
* @NoAdminRequired
* @UserRateThrottle(limit=10, period=120)
*/
public function create(string $path): DataResponse {
$pathHash = sha1($path);

$entity = new OpenLocalEditor();
$entity->setUserId($this->userId);
$entity->setPathHash($pathHash);
$entity->setExpirationTime($this->timeFactory->getTime() + self::TOKEN_DURATION); // Expire in 10 minutes

for ($i = 1; $i <= self::TOKEN_RETRIES; $i++) {
$token = $this->secureRandom->generate(self::TOKEN_LENGTH, ISecureRandom::CHAR_ALPHANUMERIC);
$entity->setToken($token);

try {
$this->mapper->insert($entity);

return new DataResponse([
'userId' => $this->userId,
'pathHash' => $pathHash,
'expirationTime' => $entity->getExpirationTime(),
'token' => $entity->getToken(),
]);
} catch (Exception $e) {
if ($e->getCode() !== Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
// Only retry on unique constraint violation
throw $e;
}
}
}

$this->logger->error('Giving up after ' . self::TOKEN_RETRIES . ' retries to generate a unique local editor token for path hash: ' . $pathHash);
return new DataResponse([], Http::STATUS_INTERNAL_SERVER_ERROR);
}

/**
* @NoAdminRequired
* @BruteForceProtection(action=openLocalEditor)
*/
public function validate(string $path, string $token): DataResponse {
$pathHash = sha1($path);

try {
$entity = $this->mapper->verifyToken($this->userId, $pathHash, $token);
} catch (DoesNotExistException $e) {
$response = new DataResponse([], Http::STATUS_NOT_FOUND);
$response->throttle(['userId' => $this->userId, 'pathHash' => $pathHash]);
return $response;
}

if ($entity->getExpirationTime() <= $this->timeFactory->getTime()) {
$this->mapper->delete($entity);
return new DataResponse([], Http::STATUS_NOT_FOUND);
}

return new DataResponse([
'userId' => $this->userId,
'pathHash' => $pathHash,
'expirationTime' => $entity->getExpirationTime(),
'token' => $entity->getToken(),
]);
}

}

+ 60
- 0
apps/files/lib/Db/OpenLocalEditor.php View File

@@ -0,0 +1,60 @@
<?php

declare(strict_types=1);

/**
* @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

namespace OCA\Files\Db;

use OCP\AppFramework\Db\Entity;

/**
* @method void setUserId(string $userId)
* @method string getUserId()
* @method void setPathHash(string $pathHash)
* @method string getPathHash()
* @method void setExpirationTime(int $expirationTime)
* @method int getExpirationTime()
* @method void setToken(string $token)
* @method string getToken()
*/
class OpenLocalEditor extends Entity {
/** @var string */
protected $userId;

/** @var string */
protected $pathHash;

/** @var int */
protected $expirationTime;

/** @var string */
protected $token;

public function __construct() {
$this->addType('userId', 'string');
$this->addType('pathHash', 'string');
$this->addType('expirationTime', 'integer');
$this->addType('token', 'string');
}
}

+ 56
- 0
apps/files/lib/Db/OpenLocalEditorMapper.php View File

@@ -0,0 +1,56 @@
<?php

declare(strict_types=1);

/**
* @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

namespace OCA\Files\Db;

use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\AppFramework\Db\QBMapper;
use OCP\DB\Exception;
use OCP\IDBConnection;

class OpenLocalEditorMapper extends QBMapper {
public function __construct(IDBConnection $db) {
parent::__construct($db, 'open_local_editor', OpenLocalEditor::class);
}

/**
* @throws DoesNotExistException
* @throws MultipleObjectsReturnedException
* @throws Exception
*/
public function verifyToken(string $userId, string $pathHash, string $token): OpenLocalEditor {
$qb = $this->db->getQueryBuilder();

$qb->select('*')
->from($this->getTableName())
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId)))
->andWhere($qb->expr()->eq('path_hash', $qb->createNamedParameter($pathHash)))
->andWhere($qb->expr()->eq('token', $qb->createNamedParameter($token)));

return $this->findEntity($qb);
}
}

+ 75
- 0
apps/files/lib/Migration/Version12101Date20221011153334.php View File

@@ -0,0 +1,75 @@
<?php

declare(strict_types=1);

/**
* @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

namespace OCA\Files\Migration;

use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;

class Version12101Date20221011153334 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param Closure(): ISchemaWrapper $schemaClosure
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();

$table = $schema->createTable('open_local_editor');
$table->addColumn('id',Types::BIGINT, [
'autoincrement' => true,
'notnull' => true,
'length' => 20,
'unsigned' => true,
]);
$table->addColumn('user_id', Types::STRING, [
'notnull' => true,
'length' => 64,
]);
$table->addColumn('path_hash', Types::STRING, [
'notnull' => true,
'length' => 64,
]);
$table->addColumn('expiration_time', Types::BIGINT, [
'notnull' => true,
'unsigned' => true,
]);
$table->addColumn('token', Types::STRING, [
'notnull' => true,
'length' => 128,
]);

$table->setPrimaryKey(['id']);
$table->addUniqueIndex(['user_id', 'path_hash', 'token'], 'openlocal_user_path_token');

return $schema;
}
}

Loading…
Cancel
Save