Skip to content

Commit

Permalink
Put infrastructure in place to restrict access to a OO document serve…
Browse files Browse the repository at this point in the history
…r to some projects

All what is needed in the backend side is in place to make possible to
restrict a OO document server to some specific projects.
No changes are made yet in the web UI so there is no user facing changes
yet.

To test you can set explicitly the column `is_project_restricted` in the
table `plugin_onlyoffice_document_server` to true and add some
restrictions in the table `plugin_onlyoffice_document_server_project_restriction`.

Part of story #29981: restrict an OnlyOffice server to some projects only

Change-Id: Id62c7ae7e4f86c4391b1e1fc385b345226d9dcea
  • Loading branch information
LeSuisse committed Dec 20, 2022
1 parent 144ef62 commit 9f9e629
Show file tree
Hide file tree
Showing 23 changed files with 317 additions and 46 deletions.
11 changes: 10 additions & 1 deletion plugins/onlyoffice/db/install.sql
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,14 @@ CREATE TABLE plugin_onlyoffice_save_document_token(
CREATE TABLE plugin_onlyoffice_document_server(
id INT(11) NOT NULL PRIMARY KEY AUTO_INCREMENT,
url VARCHAR(255) NOT NULL,
secret_key TEXT NOT NULL
secret_key TEXT NOT NULL,
is_project_restricted BOOLEAN NOT NULL DEFAULT FALSE
) ENGINE=InnoDB;


CREATE TABLE plugin_onlyoffice_document_server_project_restriction(
project_id INT(11) NOT NULL,
server_id INT(11) NOT NULL,
PRIMARY KEY (project_id, server_id),
UNIQUE idx_project_id(project_id)
) ENGINE=InnoDB;
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php
/**
* Copyright (c) Enalean, 2022-Present. All Rights Reserved.
*
* This file is a part of Tuleap.
*
* Tuleap is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* Tuleap 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Tuleap. If not, see <http://www.gnu.org/licenses/>.
*/

declare(strict_types=1);

// phpcs:ignore PSR1.Classes.ClassDeclaration.MissingNamespace,Squiz.Classes.ValidClassName.NotCamelCaps
final class b202212201015_create_server_project_restriction_table extends \Tuleap\ForgeUpgrade\Bucket
{
public function description(): string
{
return 'Create table plugin_onlyoffice_document_server_project_restriction';
}

public function up(): void
{
$this->api->createTable(
'plugin_onlyoffice_document_server_project_restriction',
'CREATE TABLE plugin_onlyoffice_document_server_project_restriction(
project_id INT(11) NOT NULL,
server_id INT(11) NOT NULL,
PRIMARY KEY (project_id, server_id),
UNIQUE idx_project_id(project_id)
) ENGINE=InnoDB;'
);
if ($this->api->columnNameExists('plugin_onlyoffice_document_server', 'is_project_restricted')) {
return;
}

$this->api->dbh->exec('ALTER TABLE plugin_onlyoffice_document_server ADD COLUMN is_project_restricted BOOLEAN NOT NULL DEFAULT FALSE');
}
}
1 change: 1 addition & 0 deletions plugins/onlyoffice/db/uninstall.sql
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@
DROP TABLE IF EXISTS plugin_onlyoffice_download_document_token;
DROP TABLE IF EXISTS plugin_onlyoffice_save_document_token;
DROP TABLE IF EXISTS plugin_onlyoffice_document_server;
DROP TABLE IF EXISTS plugin_onlyoffice_document_server_project_restriction;
DELETE FROM forgeconfig WHERE name = 'onlyoffice_document_server_url';
DELETE FROM forgeconfig WHERE name = 'onlyoffice_document_server_secret';
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ public function isOnlyOfficeIntegrationAvailableForProject(\Project $project): b
return false;
}

return true;
foreach ($servers as $server) {
if ($server->isProjectAllowed($project)) {
return true;
}
}

return false;
}
}
38 changes: 36 additions & 2 deletions plugins/onlyoffice/include/DocumentServer/DocumentServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,42 @@ final class DocumentServer
{
public bool $has_existing_secret;

public function __construct(public int $id, public string $url, public ConcealedString $encrypted_secret_key)

/**
* @param list<int> $project_restrictions
*/
private function __construct(
public int $id,
public string $url,
public ConcealedString $encrypted_secret_key,
private bool $is_project_restricted,
private array $project_restrictions,
) {
$this->has_existing_secret = ! $this->encrypted_secret_key->isIdenticalTo(new ConcealedString(''));
}

/**
* @param list<int> $project_restrictions
*/
public static function withProjectRestrictions(
int $id,
string $url,
ConcealedString $encrypted_secret_key,
array $project_restrictions,
): self {
return new self($id, $url, $encrypted_secret_key, true, $project_restrictions);
}

public static function withoutProjectRestrictions(
int $id,
string $url,
ConcealedString $encrypted_secret_key,
): self {
return new self($id, $url, $encrypted_secret_key, false, []);
}

public function isProjectAllowed(\Project $project): bool
{
$this->has_existing_secret = $this->encrypted_secret_key->getString() !== '';
return ! $this->is_project_restricted || in_array((int) $project->getID(), $this->project_restrictions, true);
}
}
85 changes: 76 additions & 9 deletions plugins/onlyoffice/include/DocumentServer/DocumentServerDao.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

namespace Tuleap\OnlyOffice\DocumentServer;

use ParagonIE\EasyDB\EasyDB;
use Tuleap\Cryptography\ConcealedString;
use Tuleap\DB\DataAccessObject;

Expand All @@ -37,35 +38,101 @@ public function __construct(private DocumentServerKeyEncryption $encryption)
*/
public function retrieveAll(): array
{
return array_map(
static fn (array $row): DocumentServer => new DocumentServer($row['id'], $row['url'], new ConcealedString($row['secret_key'])),
$this->getDB()->run('SELECT * FROM plugin_onlyoffice_document_server ORDER BY url')
$document_servers = [];

$server_restrictions = $this->getDB()->safeQuery(
'SELECT server_id, project_id
FROM plugin_onlyoffice_document_server_project_restriction',
[],
\PDO::FETCH_GROUP | \PDO::FETCH_COLUMN
);
$server_rows = $this->getDB()->run('SELECT id, url, secret_key, is_project_restricted FROM plugin_onlyoffice_document_server ORDER BY url');

foreach ($server_rows as $server_row) {
$server_id = $server_row['id'];
$secret_key = new ConcealedString($server_row['secret_key']);
sodium_memzero($server_row['secret_key']);

if ($server_row['is_project_restricted'] || count($server_rows) > 1) {
$document_servers[] = DocumentServer::withProjectRestrictions(
$server_id,
$server_row['url'],
$secret_key,
$server_restrictions[$server_id] ?? []
);
} else {
$document_servers[] = DocumentServer::withoutProjectRestrictions(
$server_id,
$server_row['url'],
$secret_key,
);
}
}

return $document_servers;
}

/**
* @throws DocumentServerNotFoundException
*/
public function retrieveById(int $id): DocumentServer
{
$row = $this->getDB()->row('SELECT * FROM plugin_onlyoffice_document_server WHERE id = ?', $id);
$row = $this->getDB()->row('SELECT url, secret_key, is_project_restricted FROM plugin_onlyoffice_document_server WHERE id = ?', $id);
if (! $row) {
throw new DocumentServerNotFoundException();
}

return new DocumentServer($row['id'], $row['url'], new ConcealedString($row['secret_key']));
$secret_key = new ConcealedString($row['secret_key']);
sodium_memzero($row['secret_key']);

if ($row['is_project_restricted'] || $this->isThereMultipleServers()) {
$project_restrictions = $this->getDB()->safeQuery(
'SELECT project_id
FROM plugin_onlyoffice_document_server_project_restriction
WHERE server_id=?',
[$id],
\PDO::FETCH_COLUMN
);

return DocumentServer::withProjectRestrictions(
$id,
$row['url'],
$secret_key,
$project_restrictions
);
}

return DocumentServer::withoutProjectRestrictions($id, $row['url'], $secret_key);
}

private function isThereMultipleServers(): bool
{
return $this->getDB()->cell('SELECT COUNT(id) FROM plugin_onlyoffice_document_server') > 1;
}

public function delete(int $id): void
{
$this->getDB()->delete('plugin_onlyoffice_document_server', ['id' => $id]);
$this->getDB()->run(
'DELETE plugin_onlyoffice_document_server.*, plugin_onlyoffice_document_server_project_restriction.*
FROM plugin_onlyoffice_document_server
LEFT JOIN plugin_onlyoffice_document_server_project_restriction ON (plugin_onlyoffice_document_server.id = plugin_onlyoffice_document_server_project_restriction.server_id)
WHERE plugin_onlyoffice_document_server.id = ?',
$id
);
}

public function create(string $url, ConcealedString $secret_key): void
{
$this->getDB()->insert(
'plugin_onlyoffice_document_server',
['url' => $url, 'secret_key' => $this->encryption->encryptValue($secret_key)]
$this->getDB()->tryFlatTransaction(
function (EasyDB $db) use ($url, $secret_key): void {
$db->insert(
'plugin_onlyoffice_document_server',
['url' => $url, 'secret_key' => $this->encryption->encryptValue($secret_key), 'is_project_restricted' => false]
);
if ($this->isThereMultipleServers()) {
$db->run('UPDATE plugin_onlyoffice_document_server SET is_project_restricted = TRUE');
}
}
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php
/**
* Copyright (c) Enalean, 2022 - Present. All Rights Reserved.
*
* This file is a part of Tuleap.
*
* Tuleap is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* Tuleap 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Tuleap. If not, see <http://www.gnu.org/licenses/>.
*/

declare(strict_types=1);

namespace Tuleap\OnlyOffice\DocumentServer;

use Tuleap\DB\DataAccessObject;

class DocumentServerProjectRestrictionDAO extends DataAccessObject
{
public function removeProjectFromRestriction(\Project $project): void
{
$this->getDB()->delete('plugin_onlyoffice_document_server_project_restriction', ['project_id' => $project->getID()]);
}
}
10 changes: 10 additions & 0 deletions plugins/onlyoffice/include/onlyofficePlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
use Laminas\HttpHandlerRunner\Emitter\SapiStreamEmitter;
use Tuleap\Admin\AdminPageRenderer;
use Tuleap\admin\ProjectEdit\ProjectStatusUpdate;
use Tuleap\Admin\SiteAdministrationAddOption;
use Tuleap\Admin\SiteAdministrationPluginOption;
use Tuleap\Authentication\SplitToken\PrefixedSplitTokenSerializer;
Expand Down Expand Up @@ -55,6 +56,7 @@
use Tuleap\OnlyOffice\Administration\OnlyOfficeUpdateAdminSettingsController;
use Tuleap\OnlyOffice\DocumentServer\DocumentServerDao;
use Tuleap\OnlyOffice\DocumentServer\DocumentServerKeyEncryption;
use Tuleap\OnlyOffice\DocumentServer\DocumentServerProjectRestrictionDAO;
use Tuleap\OnlyOffice\Download\DownloadDocumentWithTokenMiddleware;
use Tuleap\OnlyOffice\Download\OnlyOfficeDownloadDocumentTokenDAO;
use Tuleap\OnlyOffice\Download\OnlyOfficeDownloadDocumentTokenVerifier;
Expand Down Expand Up @@ -120,6 +122,7 @@ public function getHooksAndCallbacks(): Collection
$this->addHook(SiteAdministrationAddOption::NAME);
$this->addHook(ShouldDisplaySourceColumnForFileVersions::NAME);
$this->addHook(NewItemAlternativeCollector::NAME);
$this->addHook(ProjectStatusUpdate::NAME);
return parent::getHooksAndCallbacks();
}

Expand Down Expand Up @@ -473,6 +476,13 @@ public function siteAdministrationAddOption(SiteAdministrationAddOption $site_ad
);
}

public function projectStatusUpdate(ProjectStatusUpdate $event): void
{
if ($event->status === \Project::STATUS_DELETED) {
(new DocumentServerProjectRestrictionDAO())->removeProjectFromRestriction($event->project);
}
}

private static function getLogger(): \Psr\Log\LoggerInterface
{
return \BackendLogger::getDefaultLogger(self::LOG_IDENTIFIER);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,15 @@ public function testDocumentServerStorage(): void
{
// Create
$this->dao->create('https://example.com', new ConcealedString('very_secret'));

$servers = $this->dao->retrieveAll();
self::assertCount(1, $servers);
$server = $this->dao->retrieveById($servers[0]->id);
self::assertEquals([0], $this->getServerProjectRestrictions());

$this->dao->create('https://example.com/1', new ConcealedString('much_secret'));

// Retrieve
$server = $this->dao->retrieveById(1);
self::assertEquals('https://example.com', $server->url);
self::assertEquals('very_secret', $this->decrypt($server->encrypted_secret_key));

Expand All @@ -70,6 +75,7 @@ public function testDocumentServerStorage(): void
self::assertEquals('very_secret', $this->decrypt($servers[0]->encrypted_secret_key));
self::assertEquals('https://example.com/1', $servers[1]->url);
self::assertEquals('much_secret', $this->decrypt($servers[1]->encrypted_secret_key));
self::assertEquals([1, 1], $this->getServerProjectRestrictions());

// Update
$this->dao->update($servers[0]->id, $servers[0]->url, new ConcealedString('new_secret'));
Expand All @@ -89,4 +95,9 @@ private function decrypt(ConcealedString $secret): string
{
return $this->encryption->decryptValue($secret->getString())->getString();
}

private function getServerProjectRestrictions(): array
{
return DBFactory::getMainTuleapDBConnection()->getDB()->col('SELECT is_project_restricted FROM plugin_onlyoffice_document_server');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ private static function buildController(AdminPageRenderer $admin_page_renderer,
$admin_page_renderer,
ProvideCurrentUserStub::buildWithUser($current_user),
new OnlyOfficeAdminSettingsPresenter(
[OnlyOfficeServerPresenter::fromServer(new DocumentServer(1, 'https://onlyoffice.example.com/', new ConcealedString('123456')))],
[OnlyOfficeServerPresenter::fromServer(DocumentServer::withoutProjectRestrictions(1, 'https://onlyoffice.example.com/', new ConcealedString('123456')))],
CSRFSynchronizerTokenPresenter::fromToken(new \CSRFSynchronizerToken('/admin', '', $csrf_store)),
),
new IncludeViteAssets(__DIR__ . '/../frontend-assets/', '/assets/onlyoffice'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ class OnlyOfficeAdminSettingsPresenterBuilderTest extends TestCase
public function testGetPresenter(): void
{
$retriever = IRetrieveDocumentServersStub::buildWith(
new DocumentServer(1, 'https://example.com/1', new ConcealedString('')),
new DocumentServer(2, 'https://example.com/2', new ConcealedString('123456')),
DocumentServer::withoutProjectRestrictions(1, 'https://example.com/1', new ConcealedString('')),
DocumentServer::withoutProjectRestrictions(2, 'https://example.com/2', new ConcealedString('123456')),
);

$presenter = (new OnlyOfficeAdminSettingsPresenterBuilder($retriever))
Expand Down

0 comments on commit 9f9e629

Please sign in to comment.