Listen for event during preview rendering and apply secure view options

Signed-off-by: Julius Härtl <jus@bitgrid.net>
This commit is contained in:
Julius Härtl 2022-10-25 12:54:27 +02:00
Родитель 45fcd99ae3
Коммит 25fd88bcb0
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4C614C6ED2CDE6DF
7 изменённых файлов: 235 добавлений и 94 удалений

Просмотреть файл

@ -27,6 +27,7 @@ namespace OCA\Richdocuments\AppInfo;
use OCA\Files_Sharing\Event\ShareLinkAccessedEvent;
use OCA\Richdocuments\AppConfig;
use OCA\Richdocuments\Capabilities;
use OCA\Richdocuments\Listener\BeforeFetchPreviewListener;
use OCA\Richdocuments\Listener\CSPListener;
use OCA\Richdocuments\Listener\LoadViewerListener;
use OCA\Richdocuments\Listener\ShareLinkListener;
@ -52,6 +53,7 @@ use OCP\Files\Template\TemplateFileCreator;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IPreview;
use OCP\Preview\BeforeFetchPreviewEvent;
use OCP\Security\CSP\AddContentSecurityPolicyEvent;
class Application extends App implements IBootstrap {
@ -70,6 +72,7 @@ class Application extends App implements IBootstrap {
$context->registerEventListener(AddContentSecurityPolicyEvent::class, CSPListener::class);
$context->registerEventListener(LoadViewer::class, LoadViewerListener::class);
$context->registerEventListener(ShareLinkAccessedEvent::class, ShareLinkListener::class);
$context->registerEventListener(BeforeFetchPreviewEvent::class, BeforeFetchPreviewListener::class);
}
public function boot(IBootContext $context): void {

Просмотреть файл

@ -67,6 +67,7 @@ use OCP\Lock\LockedException;
use OCP\PreConditionNotMetException;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IManager as IShareManager;
use OCP\Share\IShare;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
@ -234,7 +235,8 @@ class WopiController extends Controller {
$response['TemplateSaveAs'] = $file->getName();
}
if ($this->shouldWatermark($isPublic, $wopi->getEditorUid(), $fileId, $wopi)) {
$share = $this->getShareForWopiToken($wopi);
if ($this->permissionManager->shouldWatermark($file, $wopi->getEditorUid(), $share)) {
$email = $user !== null && !$isPublic ? $user->getEMailAddress() : "";
$replacements = [
'userId' => $wopi->getEditorUid(),
@ -318,62 +320,6 @@ class WopiController extends Controller {
return $response;
}
private function shouldWatermark($isPublic, $userId, $fileId, Wopi $wopi) {
if ($this->config->getAppValue(AppConfig::WATERMARK_APP_NAMESPACE, 'watermark_enabled', 'no') === 'no') {
return false;
}
if ($isPublic) {
if ($this->config->getAppValue(AppConfig::WATERMARK_APP_NAMESPACE, 'watermark_linkAll', 'no') === 'yes') {
return true;
}
if ($this->config->getAppValue(AppConfig::WATERMARK_APP_NAMESPACE, 'watermark_linkRead', 'no') === 'yes' && !$wopi->getCanwrite()) {
return true;
}
if ($this->config->getAppValue(AppConfig::WATERMARK_APP_NAMESPACE, 'watermark_linkSecure', 'no') === 'yes' && $wopi->getHideDownload()) {
return true;
}
if ($this->config->getAppValue(AppConfig::WATERMARK_APP_NAMESPACE, 'watermark_linkTags', 'no') === 'yes') {
$tags = $this->appConfig->getAppValueArray('watermark_linkTagsList');
$fileTags = \OC::$server->getSystemTagObjectMapper()->getTagIdsForObjects([$fileId], 'files')[$fileId];
foreach ($fileTags as $tagId) {
if (in_array($tagId, $tags, true)) {
return true;
}
}
}
} else {
if ($this->config->getAppValue(AppConfig::WATERMARK_APP_NAMESPACE, 'watermark_shareAll', 'no') === 'yes') {
$files = $this->rootFolder->getUserFolder($userId)->getById($fileId);
if (count($files) !== 0 && $files[0]->getOwner()->getUID() !== $userId) {
return true;
}
}
if ($this->config->getAppValue(AppConfig::WATERMARK_APP_NAMESPACE, 'watermark_shareRead', 'no') === 'yes' && !$wopi->getCanwrite()) {
return true;
}
}
if ($userId !== null && $this->config->getAppValue(AppConfig::WATERMARK_APP_NAMESPACE, 'watermark_allGroups', 'no') === 'yes') {
$groups = $this->appConfig->getAppValueArray('watermark_allGroupsList');
foreach ($groups as $group) {
if (\OC::$server->getGroupManager()->isInGroup($userId, $group)) {
return true;
}
}
}
if ($this->config->getAppValue(AppConfig::WATERMARK_APP_NAMESPACE, 'watermark_allTags', 'no') === 'yes') {
$tags = $this->appConfig->getAppValueArray('watermark_allTagsList');
$fileTags = \OC::$server->getSystemTagObjectMapper()->getTagIdsForObjects([$fileId], 'files')[$fileId];
foreach ($fileTags as $tagId) {
if (in_array($tagId, $tags, true)) {
return true;
}
}
}
return false;
}
/**
* Given an access token and a fileId, returns the contents of the file.
* Expects a valid token in access_token parameter.
@ -842,6 +788,7 @@ class WopiController extends Controller {
* @throws ShareNotFound
*/
private function getFileForWopiToken(Wopi $wopi) {
$this->userScopeService->setUserScope($wopi->getEditorUid());
if (!empty($wopi->getShare())) {
$share = $this->shareManager->getShareByToken($wopi->getShare());
$node = $share->getNode();
@ -875,6 +822,15 @@ class WopiController extends Controller {
return array_shift($files);
}
private function getShareForWopiToken(Wopi $wopi): ?IShare {
try {
return $wopi->getShare() ? $this->shareManager->getShareByToken($wopi->getShare()) : null;
} catch (ShareNotFound $e) {
}
return null;
}
/**
* Endpoint to return the template file that is requested by collabora to create a new document
*

Просмотреть файл

@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @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\Richdocuments\Listener;
use OCA\Files_Sharing\SharedStorage;
use OCA\Richdocuments\PermissionManager;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Files\NotFoundException;
use OCP\IRequest;
use OCP\IUserSession;
use OCP\Preview\BeforeFetchPreviewEvent;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IManager;
use OCP\Share\IShare;
class BeforeFetchPreviewListener implements IEventListener {
private PermissionManager $permissionManager;
private IUserSession $userSession;
private IRequest $request;
private IManager $shareManager;
public function __construct(PermissionManager $permissionManager, IUserSession $userSession, IRequest $request, IManager $shareManager) {
$this->permissionManager = $permissionManager;
$this->userSession = $userSession;
$this->request = $request;
$this->shareManager = $shareManager;
}
public function handle(Event $event): void {
if (!$event instanceof BeforeFetchPreviewEvent) {
return;
}
$shareToken = $this->request->getParam('token');
$share = null;
// Get share for internal shares
$storage = $event->getNode()->getStorage();
if (!$shareToken && $storage->instanceOfStorage(SharedStorage::class)) {
if (method_exists(IShare::class, 'getAttributes')) {
/** @var SharedStorage $storage */
$share = $storage->getShare();
}
}
// Get different share for public previews as the share from the node is only set for mounted shares
try {
$share = $shareToken ? $this->shareManager->getShareByToken($shareToken) : $share;
} catch (ShareNotFound $e) {
}
$userId = $this->userSession->getUser() ? $this->userSession->getUser()->getUID() : null;
if ($this->permissionManager->shouldWatermark($event->getNode(), $userId, $share)) {
throw new NotFoundException();
}
}
}

Просмотреть файл

@ -23,26 +23,38 @@ declare(strict_types=1);
namespace OCA\Richdocuments;
use OCP\Constants;
use OCP\Files\Node;
use OCP\IConfig;
use OCP\IGroupManager;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\Share\IAttributes;
use OCP\Share\IShare;
use OCP\SystemTag\ISystemTagObjectMapper;
class PermissionManager {
private AppConfig $config;
private AppConfig $appConfig;
private IConfig $config;
private IGroupManager $groupManager;
private IUserManager $userManager;
private IUserSession $userSession;
private ISystemTagObjectMapper $systemTagObjectMapper;
public function __construct(
AppConfig $config,
IGroupManager $groupManager,
IUserManager $userManager,
IUserSession $userSession
AppConfig $appConfig,
IConfig $config,
IGroupManager $groupManager,
IUserManager $userManager,
IUserSession $userSession,
ISystemTagObjectMapper $systemTagObjectMapper
) {
$this->appConfig = $appConfig;
$this->config = $config;
$this->groupManager = $groupManager;
$this->userManager = $userManager;
$this->userSession = $userSession;
$this->systemTagObjectMapper = $systemTagObjectMapper;
}
private function userMatchesGroupList(?string $userId = null, ?array $groupList = []): bool {
@ -75,7 +87,7 @@ class PermissionManager {
}
public function isEnabledForUser(string $userId = null): bool {
if ($this->userMatchesGroupList($userId, $this->config->getUseGroups())) {
if ($this->userMatchesGroupList($userId, $this->appConfig->getUseGroups())) {
return true;
}
@ -83,7 +95,7 @@ class PermissionManager {
}
public function userCanEdit(string $userId = null): bool {
if ($this->userMatchesGroupList($userId, $this->config->getEditGroups())) {
if ($this->userMatchesGroupList($userId, $this->appConfig->getEditGroups())) {
return true;
}
@ -91,10 +103,80 @@ class PermissionManager {
}
public function userIsFeatureLocked(string $userId = null): bool {
if ($this->config->isReadOnlyFeatureLocked() && !$this->userCanEdit($userId)) {
if ($this->appConfig->isReadOnlyFeatureLocked() && !$this->userCanEdit($userId)) {
return true;
}
return false;
}
public function shouldWatermark(Node $node, ?string $userId = null, ?IShare $share = null): bool {
if ($this->config->getAppValue(AppConfig::WATERMARK_APP_NAMESPACE, 'watermark_enabled', 'no') === 'no') {
return false;
}
$fileId = $node->getId();
$isUpdatable = $node->isUpdateable() && (!$share || $share->getPermissions() & Constants::PERMISSION_UPDATE);
$hasShareAttributes = $share && method_exists($share, 'getAttributes') && $share->getAttributes() instanceof IAttributes;
$isDisabledDownload = $hasShareAttributes && $share->getAttributes()->getAttribute('permissions', 'download') === false;
$isHideDownload = $share && $share->getHideDownload();
$isSecureView = $isDisabledDownload || $isHideDownload;
if ($share && $share->getShareType() === IShare::TYPE_LINK) {
if ($this->config->getAppValue(AppConfig::WATERMARK_APP_NAMESPACE, 'watermark_linkAll', 'no') === 'yes') {
return true;
}
if ($this->config->getAppValue(AppConfig::WATERMARK_APP_NAMESPACE, 'watermark_linkRead', 'no') === 'yes' && !$isUpdatable) {
return true;
}
if ($this->config->getAppValue(AppConfig::WATERMARK_APP_NAMESPACE, 'watermark_linkSecure', 'no') === 'yes' && $isSecureView) {
return true;
}
if ($this->config->getAppValue(AppConfig::WATERMARK_APP_NAMESPACE, 'watermark_linkTags', 'no') === 'yes') {
$tags = $this->appConfig->getAppValueArray('watermark_linkTagsList');
$fileTags = $this->systemTagObjectMapper->getTagIdsForObjects([$fileId], 'files')[$fileId];
foreach ($fileTags as $tagId) {
if (in_array($tagId, $tags, true)) {
return true;
}
}
}
}
if ($this->config->getAppValue(AppConfig::WATERMARK_APP_NAMESPACE, 'watermark_shareAll', 'no') === 'yes') {
if ($node->getOwner()->getUID() !== $userId) {
return true;
}
}
if ($this->config->getAppValue(AppConfig::WATERMARK_APP_NAMESPACE, 'watermark_shareRead', 'no') === 'yes' && !$isUpdatable) {
return true;
}
if ($this->config->getAppValue(AppConfig::WATERMARK_APP_NAMESPACE, 'watermark_shareDisabledDownload', 'no') === 'yes' && $isDisabledDownload) {
return true;
}
if ($userId !== null && $this->config->getAppValue(AppConfig::WATERMARK_APP_NAMESPACE, 'watermark_allGroups', 'no') === 'yes') {
$groups = $this->appConfig->getAppValueArray('watermark_allGroupsList');
foreach ($groups as $group) {
if ($this->groupManager->isInGroup($userId, $group)) {
return true;
}
}
}
if ($this->config->getAppValue(AppConfig::WATERMARK_APP_NAMESPACE, 'watermark_allTags', 'no') === 'yes') {
$tags = $this->appConfig->getAppValueArray('watermark_allTagsList');
$fileTags = $this->systemTagObjectMapper->getTagIdsForObjects([$fileId], 'files')[$fileId];
foreach ($fileTags as $tagId) {
if (in_array($tagId, $tags, true)) {
return true;
}
}
}
return false;
}
}

Просмотреть файл

@ -28,10 +28,13 @@ use OCP\IGroupManager;
use OCP\IUser;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\SystemTag\ISystemTagObjectMapper;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class PermissionManagerTest extends TestCase {
/** @var AppConfig|MockObject */
private $appConfig;
/** @var IConfig|MockObject */
private $config;
/** @var IGroupManager|MockObject */
@ -45,15 +48,17 @@ class PermissionManagerTest extends TestCase {
public function setUp(): void {
parent::setUp();
$this->config = $this->createMock(AppConfig::class);
$this->appConfig = $this->createMock(AppConfig::class);
$this->config = $this->createMock(IConfig::class);
$this->groupManager = $this->createMock(IGroupManager::class);
$this->userManager = $this->createMock(IUserManager::class);
$this->userSession = $this->createMock(IUserSession::class);
$this->permissionManager = new PermissionManager($this->config, $this->groupManager, $this->userManager, $this->userSession);
$this->systemTagMapper = $this->createMock(ISystemTagObjectMapper::class);
$this->permissionManager = new PermissionManager($this->appConfig, $this->config, $this->groupManager, $this->userManager, $this->userSession, $this->systemTagMapper);
}
public function testIsEnabledForUserEnabledNoRestrictions(): void {
$this->config
$this->appConfig
->expects($this->once())
->method('getUseGroups')
->willReturn(null);
@ -77,7 +82,7 @@ class PermissionManagerTest extends TestCase {
/** @dataProvider dataGroupMatchGroups */
public function testEditGroups($editGroups, $userGroups, $result): void {
$userMock = $this->createMock(IUser::class);
$this->config->expects($this->any())
$this->appConfig->expects($this->any())
->method('getEditGroups')
->willReturn($editGroups);
$this->userManager->expects($this->any())
@ -93,7 +98,7 @@ class PermissionManagerTest extends TestCase {
/** @dataProvider dataGroupMatchGroups */
public function testUseGroups($editGroups, $userGroups, $result): void {
$userMock = $this->createMock(IUser::class);
$this->config->expects($this->any())
$this->appConfig->expects($this->any())
->method('getUseGroups')
->willReturn($editGroups);
$this->userManager->expects($this->any())
@ -109,10 +114,10 @@ class PermissionManagerTest extends TestCase {
/** @dataProvider dataGroupMatchGroups */
public function testFeatureLock($editGroups, $userGroups, $result): void {
$userMock = $this->createMock(IUser::class);
$this->config->expects($this->any())
$this->appConfig->expects($this->any())
->method('getEditGroups')
->willReturn($editGroups);
$this->config->expects($this->any())
$this->appConfig->expects($this->any())
->method('isReadOnlyFeatureLocked')
->willReturn(true);
$this->userManager->expects($this->any())

Просмотреть файл

@ -22,6 +22,9 @@
<code>OpenDocument</code>
<code>Pdf</code>
</MissingDependency>
<UndefinedClass occurrences="1">
<code>BeforeFetchPreviewEvent</code>
</UndefinedClass>
</file>
<file src="lib/Command/ActivateConfig.php">
<RedundantCondition occurrences="1">
@ -76,24 +79,16 @@
<code>$item-&gt;getId()</code>
<code>$node-&gt;getId()</code>
</InvalidScalarArgument>
<MissingDependency occurrences="7">
<MissingDependency occurrences="5">
<code>$this-&gt;rootFolder</code>
<code>$this-&gt;rootFolder</code>
<code>$this-&gt;rootFolder</code>
<code>$this-&gt;rootFolder</code>
<code>BeforeFederationRedirectEvent</code>
<code>IRootFolder</code>
<code>IRootFolder</code>
</MissingDependency>
<RedundantCondition occurrences="1">
<code>$app !== ''</code>
</RedundantCondition>
<UndefinedClass occurrences="1">
<code>\OCA\Files_Sharing\External\Storage</code>
</UndefinedClass>
<UndefinedInterfaceMethod occurrences="1">
<code>getRemote</code>
</UndefinedInterfaceMethod>
</file>
<file src="lib/Controller/OCSController.php">
<MissingDependency occurrences="4">
@ -129,9 +124,7 @@
</UndefinedDocblockClass>
</file>
<file src="lib/Controller/WopiController.php">
<MissingDependency occurrences="13">
<code>$this-&gt;rootFolder</code>
<code>$this-&gt;rootFolder</code>
<MissingDependency occurrences="11">
<code>$this-&gt;rootFolder</code>
<code>$this-&gt;rootFolder</code>
<code>$this-&gt;rootFolder</code>
@ -159,6 +152,16 @@
<code>$time</code>
</InvalidScalarArgument>
</file>
<file src="lib/Listener/BeforeFetchPreviewListener.php">
<UndefinedClass occurrences="1">
<code>BeforeFetchPreviewEvent</code>
</UndefinedClass>
</file>
<file src="lib/PermissionManager.php">
<RedundantCondition occurrences="1">
<code>$share &amp;&amp; method_exists($share, 'getAttributes')</code>
</RedundantCondition>
</file>
<file src="lib/Preview/MSExcel.php">
<MissingDependency occurrences="1">
<code>Office</code>

Просмотреть файл

@ -3,31 +3,40 @@
declare(strict_types=1);
namespace OCA\Federation {
class TrustedServers {
public function getServers() {}
public function isTrustedServer($domainWithPort) {}
}
class TrustedServers {
public function getServers() {
}
public function isTrustedServer($domainWithPort) {
}
}
}
namespace OCA\Viewer\Event {
class LoadViewer extends \OCP\EventDispatcher\Event {}
class LoadViewer extends \OCP\EventDispatcher\Event {
}
}
namespace Doctrine\DBAL\Platforms {
class SqlitePlatform {}
class SqlitePlatform {
}
}
namespace OCA\Files_Sharing {
use OCP\Files\Storage\IStorage;
use \OCP\Share\IShare;
class SharedStorage {
public function getShare(): IShare {}
abstract class SharedStorage implements IStorage {
public function getShare(): IShare {
}
}
}
namespace OCA\Files_Sharing\Event {
use \OCP\Share\IShare;
class ShareLinkAccessedEvent extends \OCP\EventDispatcher\Event {
public function __construct(IShare $share, string $step = '', int $errorCode = 200, string $errorMessage = '') {}
public function __construct(IShare $share, string $step = '', int $errorCode = 200, string $errorMessage = '') {
}
public function getShare(): IShare {
}
@ -44,5 +53,6 @@ namespace OCA\Files_Sharing\Event {
}
class OC_Helper {
public static function getFileTemplateManager() {}
public static function getFileTemplateManager() {
}
}