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

refactor(files_sharing): Use events for share access #48601

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
221 changes: 18 additions & 203 deletions apps/files_sharing/lib/Controller/ShareController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,18 @@
use OC\Security\CSP\ContentSecurityPolicy;
use OCA\DAV\Connector\Sabre\PublicAuth;
use OCA\FederatedFileSharing\FederatedShareProvider;
use OCA\Files_Sharing\Activity\Providers\Downloads;
use OCA\Files_Sharing\Event\BeforeTemplateRenderedEvent;
use OCA\Files_Sharing\Event\ShareLinkAccessedEvent;
use OCA\Files_Sharing\Services\ShareAccessService;
use OCP\Accounts\IAccountManager;
use OCP\AppFramework\AuthPublicShareController;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\NotFoundResponse;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\Defaults;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\IConfig;
Expand Down Expand Up @@ -55,12 +53,12 @@ class ShareController extends AuthPublicShareController {
public function __construct(
string $appName,
IRequest $request,
protected IConfig $config,
ISession $session,
IURLGenerator $urlGenerator,
protected IConfig $config,
protected IUserManager $userManager,
protected \OCP\Activity\IManager $activityManager,
protected ShareManager $shareManager,
ISession $session,
protected IPreview $previewManager,
protected IRootFolder $rootFolder,
protected FederatedShareProvider $federatedShareProvider,
Expand All @@ -70,6 +68,7 @@ public function __construct(
protected ISecureRandom $secureRandom,
protected Defaults $defaults,
private IPublicShareTemplateFactory $publicShareTemplateFactory,
private ShareAccessService $accessService,
) {
parent::__construct($appName, $request, $session, $urlGenerator);
}
Expand Down Expand Up @@ -195,64 +194,9 @@ protected function authSucceeded() {
$this->session->set(PublicAuth::DAV_AUTHENTICATED, $this->share->getId());
}

/** @inheritDoc */
protected function authFailed() {
$this->emitAccessShareHook($this->share, 403, 'Wrong password');
$this->emitShareAccessEvent($this->share, self::SHARE_AUTH, 403, 'Wrong password');
}

/**
* throws hooks when a share is attempted to be accessed
*
* @param \OCP\Share\IShare|string $share the Share instance if available,
* otherwise token
* @param int $errorCode
* @param string $errorMessage
*
* @throws \OCP\HintException
* @throws \OC\ServerNotAvailableException
*
* @deprecated use OCP\Files_Sharing\Event\ShareLinkAccessedEvent
*/
protected function emitAccessShareHook($share, int $errorCode = 200, string $errorMessage = '') {
$itemType = $itemSource = $uidOwner = '';
$token = $share;
$exception = null;
if ($share instanceof \OCP\Share\IShare) {
try {
$token = $share->getToken();
$uidOwner = $share->getSharedBy();
$itemType = $share->getNodeType();
$itemSource = $share->getNodeId();
} catch (\Exception $e) {
// we log what we know and pass on the exception afterwards
$exception = $e;
}
}

\OC_Hook::emit(Share::class, 'share_link_access', [
'itemType' => $itemType,
'itemSource' => $itemSource,
'uidOwner' => $uidOwner,
'token' => $token,
'errorCode' => $errorCode,
'errorMessage' => $errorMessage
]);

if (!is_null($exception)) {
throw $exception;
}
}

/**
* Emit a ShareLinkAccessedEvent event when a share is accessed, downloaded, auth...
*/
protected function emitShareAccessEvent(IShare $share, string $step = '', int $errorCode = 200, string $errorMessage = ''): void {
if ($step !== self::SHARE_ACCESS &&
$step !== self::SHARE_AUTH &&
$step !== self::SHARE_DOWNLOAD) {
return;
}
$this->eventDispatcher->dispatchTyped(new ShareLinkAccessedEvent($share, $step, $errorCode, $errorMessage));
$this->accessService->accessWrongPassword($this->share);
}

/**
Expand All @@ -261,7 +205,7 @@ protected function emitShareAccessEvent(IShare $share, string $step = '', int $e
* @param Share\IShare $share
* @return bool
*/
private function validateShare(\OCP\Share\IShare $share) {
private function validateSharePermissions(\OCP\Share\IShare $share) {
// If the owner is disabled no access to the link is granted
$owner = $this->userManager->get($share->getShareOwner());
if ($owner === null || !$owner->isEnabled()) {
Expand Down Expand Up @@ -292,12 +236,11 @@ public function showShare($path = ''): TemplateResponse {
try {
$share = $this->shareManager->getShareByToken($this->getToken());
} catch (ShareNotFound $e) {
// The share does not exists, we do not emit an ShareLinkAccessedEvent
$this->emitAccessShareHook($this->getToken(), 404, 'Share not found');
throw new NotFoundException($this->l10n->t('This share does not exist or is no longer available'));
}

if (!$this->validateShare($share)) {
if (!$this->validateSharePermissions($share)) {
$this->accessService->shareNotFound($share);
throw new NotFoundException($this->l10n->t('This share does not exist or is no longer available'));
}

Expand All @@ -307,28 +250,17 @@ public function showShare($path = ''): TemplateResponse {
$templateProvider = $this->publicShareTemplateFactory->getProvider($share);
$response = $templateProvider->renderPage($share, $this->getToken(), $path);
} catch (NotFoundException $e) {
$this->emitAccessShareHook($share, 404, 'Share not found');
$this->emitShareAccessEvent($share, ShareController::SHARE_ACCESS, 404, 'Share not found');
$this->accessService->shareNotFound($share);
throw new NotFoundException($this->l10n->t('This share does not exist or is no longer available'));
}

// We can't get the path of a file share
try {
if ($shareNode instanceof \OCP\Files\File && $path !== '') {
$this->emitAccessShareHook($share, 404, 'Share not found');
$this->emitShareAccessEvent($share, self::SHARE_ACCESS, 404, 'Share not found');
throw new NotFoundException($this->l10n->t('This share does not exist or is no longer available'));
}
} catch (\Exception $e) {
$this->emitAccessShareHook($share, 404, 'Share not found');
$this->emitShareAccessEvent($share, self::SHARE_ACCESS, 404, 'Share not found');
throw $e;
if (($shareNode instanceof \OCP\Files\File) && $path !== '') {
$this->accessService->shareNotFound($share);
throw new NotFoundException($this->l10n->t('This share does not exist or is no longer available'));
}


$this->emitAccessShareHook($share);
$this->emitShareAccessEvent($share, self::SHARE_ACCESS);

$this->accessService->accessShare($share);
return $response;
}

Expand All @@ -349,136 +281,19 @@ public function downloadShare($token, $files = null, $path = '') {

$share = $this->shareManager->getShareByToken($token);

if (!($share->getPermissions() & \OCP\Constants::PERMISSION_READ)) {
return new \OCP\AppFramework\Http\DataResponse('Share has no read permission');
}

if (!$this->validateShare($share)) {
if (!$this->validateSharePermissions($share)) {
throw new NotFoundException();
}

// Single file share
if ($share->getNode() instanceof \OCP\Files\File) {
// Single file download
$this->singleFileDownloaded($share, $share->getNode());
}
// Directory share
else {
/** @var \OCP\Files\Folder $node */
$node = $share->getNode();

// Try to get the path
if ($path !== '') {
try {
$node = $node->get($path);
} catch (NotFoundException $e) {
$this->emitAccessShareHook($share, 404, 'Share not found');
$this->emitShareAccessEvent($share, self::SHARE_DOWNLOAD, 404, 'Share not found');
return new NotFoundResponse();
}
}

if ($node instanceof \OCP\Files\Folder) {
if ($files === null || $files === '') {
// The folder is downloaded
$this->singleFileDownloaded($share, $share->getNode());
} else {
$fileList = json_decode($files);
// in case we get only a single file
if (!is_array($fileList)) {
$fileList = [$fileList];
}
foreach ($fileList as $file) {
$subNode = $node->get($file);
$this->singleFileDownloaded($share, $subNode);
}
}
} else {
// Single file download
$this->singleFileDownloaded($share, $share->getNode());
}
if (!($share->getPermissions() & \OCP\Constants::PERMISSION_READ)) {
return new \OCP\AppFramework\Http\DataResponse('Share has no read permission', Http::STATUS_FORBIDDEN);
}

$this->emitAccessShareHook($share);
$this->emitShareAccessEvent($share, self::SHARE_DOWNLOAD);

$davUrl = '/public.php/dav/files/' . $token . '/?accept=zip';
if ($files !== null) {
$davUrl .= '&files=' . $files;
}
return new RedirectResponse($this->urlGenerator->getAbsoluteURL($davUrl));
}

/**
* create activity if a single file was downloaded from a link share
*
* @param Share\IShare $share
* @throws NotFoundException when trying to download a folder of a "hide download" share
*/
protected function singleFileDownloaded(Share\IShare $share, \OCP\Files\Node $node) {
if ($share->getHideDownload() && $node instanceof Folder) {
throw new NotFoundException('Downloading a folder');
}

$fileId = $node->getId();

$userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
$userNode = $userFolder->getFirstNodeById($fileId);
$ownerFolder = $this->rootFolder->getUserFolder($share->getShareOwner());
$userPath = $userFolder->getRelativePath($userNode->getPath());
$ownerPath = $ownerFolder->getRelativePath($node->getPath());
$remoteAddress = $this->request->getRemoteAddress();
$dateTime = new \DateTime();
$dateTime = $dateTime->format('Y-m-d H');
$remoteAddressHash = md5($dateTime . '-' . $remoteAddress);

$parameters = [$userPath];

if ($share->getShareType() === IShare::TYPE_EMAIL) {
if ($node instanceof \OCP\Files\File) {
$subject = Downloads::SUBJECT_SHARED_FILE_BY_EMAIL_DOWNLOADED;
} else {
$subject = Downloads::SUBJECT_SHARED_FOLDER_BY_EMAIL_DOWNLOADED;
}
$parameters[] = $share->getSharedWith();
} else {
if ($node instanceof \OCP\Files\File) {
$subject = Downloads::SUBJECT_PUBLIC_SHARED_FILE_DOWNLOADED;
$parameters[] = $remoteAddressHash;
} else {
$subject = Downloads::SUBJECT_PUBLIC_SHARED_FOLDER_DOWNLOADED;
$parameters[] = $remoteAddressHash;
}
}

$this->publishActivity($subject, $parameters, $share->getSharedBy(), $fileId, $userPath);

if ($share->getShareOwner() !== $share->getSharedBy()) {
$parameters[0] = $ownerPath;
$this->publishActivity($subject, $parameters, $share->getShareOwner(), $fileId, $ownerPath);
}
}

/**
* publish activity
*
* @param string $subject
* @param array $parameters
* @param string $affectedUser
* @param int $fileId
* @param string $filePath
*/
protected function publishActivity($subject,
array $parameters,
$affectedUser,
$fileId,
$filePath) {
$event = $this->activityManager->generateEvent();
$event->setApp('files_sharing')
->setType('public_links')
->setSubject($subject, $parameters)
->setAffectedUser($affectedUser)
->setObject('files', $fileId, $filePath);
$this->activityManager->publish($event);
}
}
34 changes: 17 additions & 17 deletions apps/files_sharing/lib/Event/ShareLinkAccessedEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,24 @@
use OCP\Share\IShare;

class ShareLinkAccessedEvent extends Event {
/** @var IShare */
private $share;

/** @var string */
private $step;

/** @var int */
private $errorCode;

/** @var string */
private $errorMessage;

public function __construct(IShare $share, string $step = '', int $errorCode = 200, string $errorMessage = '') {

/** @since 31.0.0 */
public const STEP_ACCESS = 'access';
/** @since 31.0.0 */
public const STEP_AUTH = 'auth';
/** @since 31.0.0 */
public const STEP_DOWNLOAD = 'download';

/**
* @param ShareLinkAccessedEvent::STEP_* $step

Check failure on line 25 in apps/files_sharing/lib/Event/ShareLinkAccessedEvent.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

InvalidParamDefault

apps/files_sharing/lib/Event/ShareLinkAccessedEvent.php:25:12: InvalidParamDefault: Default value type '' for argument 2 of method OCA\Files_Sharing\Event\ShareLinkAccessedEvent::__construct does not match the given type 'access'|'auth'|'download' (see https://psalm.dev/062)
*/
public function __construct(
private IShare $share,
private string $step = '',
private int $errorCode = 200,
private string $errorMessage = '',
) {
parent::__construct();
$this->share = $share;
$this->step = $step;
$this->errorCode = $errorCode;
$this->errorMessage = $errorMessage;
}

public function getShare(): IShare {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@

namespace OCA\Files_Sharing\Listener;

use OCA\Files_Sharing\Services\ShareAccessService;
use OCA\Files_Sharing\ViewOnly;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Files\Events\BeforeDirectFileDownloadEvent;
use OCP\Files\IRootFolder;
use OCP\Files\Storage\ISharedStorage;
use OCP\IUserSession;

/**
Expand All @@ -24,6 +26,7 @@ class BeforeDirectFileDownloadListener implements IEventListener {
public function __construct(
private IUserSession $userSession,
private IRootFolder $rootFolder,
private ShareAccessService $accessService,
) {
}

Expand All @@ -42,6 +45,19 @@ public function handle(Event $event): void {
if (!$viewOnlyHandler->check($pathsToCheck)) {
$event->setSuccessful(false);
$event->setErrorMessage('Access to this resource or one of its sub-items has been denied.');
return;
}
}

$node = $event->getNode();
if ($node !== null) {
$storage = $node->getStorage();
if ($storage->instanceOfStorage(ISharedStorage::class)) {
/** @var ISharedStorage $storage */
$share = $storage->getShare();
$this->accessService->shareDownloaded($share);
// All we now need to do is log the download
$this->accessService->sharedFileDownloaded($share, $node);
}
}
}
Expand Down
Loading
Loading