aboutsummaryrefslogtreecommitdiffstats
path: root/apps/federatedfilesharing/lib/Controller/RequestHandlerController.php
diff options
context:
space:
mode:
Diffstat (limited to 'apps/federatedfilesharing/lib/Controller/RequestHandlerController.php')
-rw-r--r--apps/federatedfilesharing/lib/Controller/RequestHandlerController.php418
1 files changed, 418 insertions, 0 deletions
diff --git a/apps/federatedfilesharing/lib/Controller/RequestHandlerController.php b/apps/federatedfilesharing/lib/Controller/RequestHandlerController.php
new file mode 100644
index 00000000000..7fdd718cbfe
--- /dev/null
+++ b/apps/federatedfilesharing/lib/Controller/RequestHandlerController.php
@@ -0,0 +1,418 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\FederatedFileSharing\Controller;
+
+use OCA\FederatedFileSharing\AddressHandler;
+use OCA\FederatedFileSharing\FederatedShareProvider;
+use OCA\FederatedFileSharing\Notifications;
+use OCP\App\IAppManager;
+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\DataResponse;
+use OCP\AppFramework\OCS\OCSBadRequestException;
+use OCP\AppFramework\OCS\OCSException;
+use OCP\AppFramework\OCSController;
+use OCP\Constants;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\Federation\Exceptions\ProviderCouldNotAddShareException;
+use OCP\Federation\Exceptions\ProviderDoesNotExistsException;
+use OCP\Federation\ICloudFederationFactory;
+use OCP\Federation\ICloudFederationProviderManager;
+use OCP\Federation\ICloudIdManager;
+use OCP\HintException;
+use OCP\IDBConnection;
+use OCP\IRequest;
+use OCP\IUserManager;
+use OCP\Log\Audit\CriticalActionPerformedEvent;
+use OCP\Server;
+use OCP\Share;
+use OCP\Share\Exceptions\ShareNotFound;
+use Psr\Log\LoggerInterface;
+
+#[OpenAPI(scope: OpenAPI::SCOPE_FEDERATION)]
+class RequestHandlerController extends OCSController {
+
+ public function __construct(
+ string $appName,
+ IRequest $request,
+ private FederatedShareProvider $federatedShareProvider,
+ private IDBConnection $connection,
+ private Share\IManager $shareManager,
+ private Notifications $notifications,
+ private AddressHandler $addressHandler,
+ private IUserManager $userManager,
+ private ICloudIdManager $cloudIdManager,
+ private LoggerInterface $logger,
+ private ICloudFederationFactory $cloudFederationFactory,
+ private ICloudFederationProviderManager $cloudFederationProviderManager,
+ private IEventDispatcher $eventDispatcher,
+ ) {
+ parent::__construct($appName, $request);
+ }
+
+ /**
+ * create a new share
+ *
+ * @param string|null $remote Address of the remote
+ * @param string|null $token Shared secret between servers
+ * @param string|null $name Name of the shared resource
+ * @param string|null $owner Display name of the receiver
+ * @param string|null $sharedBy Display name of the sender
+ * @param string|null $shareWith ID of the user that receives the share
+ * @param int|null $remoteId ID of the remote
+ * @param string|null $sharedByFederatedId Federated ID of the sender
+ * @param string|null $ownerFederatedId Federated ID of the receiver
+ * @return Http\DataResponse<Http::STATUS_OK, list<empty>, array{}>
+ * @throws OCSException
+ *
+ * 200: Share created successfully
+ */
+ #[NoCSRFRequired]
+ #[PublicPage]
+ public function createShare(
+ ?string $remote = null,
+ ?string $token = null,
+ ?string $name = null,
+ ?string $owner = null,
+ ?string $sharedBy = null,
+ ?string $shareWith = null,
+ ?int $remoteId = null,
+ ?string $sharedByFederatedId = null,
+ ?string $ownerFederatedId = null,
+ ) {
+ if ($ownerFederatedId === null) {
+ $ownerFederatedId = $this->cloudIdManager->getCloudId($owner, $this->cleanupRemote($remote))->getId();
+ }
+ // if the owner of the share and the initiator are the same user
+ // we also complete the federated share ID for the initiator
+ if ($sharedByFederatedId === null && $owner === $sharedBy) {
+ $sharedByFederatedId = $ownerFederatedId;
+ }
+
+ $share = $this->cloudFederationFactory->getCloudFederationShare(
+ $shareWith,
+ $name,
+ '',
+ $remoteId,
+ $ownerFederatedId,
+ $owner,
+ $sharedByFederatedId,
+ $sharedBy,
+ $token,
+ 'user',
+ 'file'
+ );
+
+ try {
+ $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file');
+ $provider->shareReceived($share);
+ if ($sharedByFederatedId === $ownerFederatedId) {
+ $this->eventDispatcher->dispatchTyped(new CriticalActionPerformedEvent('A new federated share with "%s" was created by "%s" and shared with "%s"', [$name, $ownerFederatedId, $shareWith]));
+ } else {
+ $this->eventDispatcher->dispatchTyped(new CriticalActionPerformedEvent('A new federated share with "%s" was shared by "%s" (resource owner is: "%s") and shared with "%s"', [$name, $sharedByFederatedId, $ownerFederatedId, $shareWith]));
+ }
+ } catch (ProviderDoesNotExistsException $e) {
+ throw new OCSException('Server does not support federated cloud sharing', 503);
+ } catch (ProviderCouldNotAddShareException $e) {
+ throw new OCSException($e->getMessage(), 400);
+ } catch (\Exception $e) {
+ throw new OCSException('internal server error, was not able to add share from ' . $remote, 500);
+ }
+
+ return new DataResponse();
+ }
+
+ /**
+ * create re-share on behalf of another user
+ *
+ * @param int $id ID of the share
+ * @param string|null $token Shared secret between servers
+ * @param string|null $shareWith ID of the user that receives the share
+ * @param int|null $remoteId ID of the remote
+ * @return Http\DataResponse<Http::STATUS_OK, array{token: string, remoteId: string}, array{}>
+ * @throws OCSBadRequestException Re-sharing is not possible
+ * @throws OCSException
+ *
+ * 200: Remote share returned
+ */
+ #[NoCSRFRequired]
+ #[PublicPage]
+ public function reShare(int $id, ?string $token = null, ?string $shareWith = null, ?int $remoteId = 0) {
+ if ($token === null
+ || $shareWith === null
+ || $remoteId === null
+ ) {
+ throw new OCSBadRequestException();
+ }
+
+ $notification = [
+ 'sharedSecret' => $token,
+ 'shareWith' => $shareWith,
+ 'senderId' => $remoteId,
+ 'message' => 'Recipient of a share ask the owner to reshare the file'
+ ];
+
+ try {
+ $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file');
+ [$newToken, $localId] = $provider->notificationReceived('REQUEST_RESHARE', $id, $notification);
+ return new DataResponse([
+ 'token' => $newToken,
+ 'remoteId' => $localId
+ ]);
+ } catch (ProviderDoesNotExistsException $e) {
+ throw new OCSException('Server does not support federated cloud sharing', 503);
+ } catch (ShareNotFound $e) {
+ $this->logger->debug('Share not found: ' . $e->getMessage(), ['exception' => $e]);
+ } catch (\Exception $e) {
+ $this->logger->debug('internal server error, can not process notification: ' . $e->getMessage(), ['exception' => $e]);
+ }
+
+ throw new OCSBadRequestException();
+ }
+
+
+ /**
+ * accept server-to-server share
+ *
+ * @param int $id ID of the remote share
+ * @param string|null $token Shared secret between servers
+ * @return Http\DataResponse<Http::STATUS_OK, list<empty>, array{}>
+ * @throws OCSException
+ * @throws ShareNotFound
+ * @throws HintException
+ *
+ * 200: Share accepted successfully
+ */
+ #[NoCSRFRequired]
+ #[PublicPage]
+ public function acceptShare(int $id, ?string $token = null) {
+ $notification = [
+ 'sharedSecret' => $token,
+ 'message' => 'Recipient accept the share'
+ ];
+
+ try {
+ $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file');
+ $provider->notificationReceived('SHARE_ACCEPTED', $id, $notification);
+ $this->eventDispatcher->dispatchTyped(new CriticalActionPerformedEvent('Federated share with id "%s" was accepted', [$id]));
+ } catch (ProviderDoesNotExistsException $e) {
+ throw new OCSException('Server does not support federated cloud sharing', 503);
+ } catch (ShareNotFound $e) {
+ $this->logger->debug('Share not found: ' . $e->getMessage(), ['exception' => $e]);
+ } catch (\Exception $e) {
+ $this->logger->debug('internal server error, can not process notification: ' . $e->getMessage(), ['exception' => $e]);
+ }
+
+ return new DataResponse();
+ }
+
+ /**
+ * decline server-to-server share
+ *
+ * @param int $id ID of the remote share
+ * @param string|null $token Shared secret between servers
+ * @return Http\DataResponse<Http::STATUS_OK, list<empty>, array{}>
+ * @throws OCSException
+ *
+ * 200: Share declined successfully
+ */
+ #[NoCSRFRequired]
+ #[PublicPage]
+ public function declineShare(int $id, ?string $token = null) {
+ $notification = [
+ 'sharedSecret' => $token,
+ 'message' => 'Recipient declined the share'
+ ];
+
+ try {
+ $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file');
+ $provider->notificationReceived('SHARE_DECLINED', $id, $notification);
+ $this->eventDispatcher->dispatchTyped(new CriticalActionPerformedEvent('Federated share with id "%s" was declined', [$id]));
+ } catch (ProviderDoesNotExistsException $e) {
+ throw new OCSException('Server does not support federated cloud sharing', 503);
+ } catch (ShareNotFound $e) {
+ $this->logger->debug('Share not found: ' . $e->getMessage(), ['exception' => $e]);
+ } catch (\Exception $e) {
+ $this->logger->debug('internal server error, can not process notification: ' . $e->getMessage(), ['exception' => $e]);
+ }
+
+ return new DataResponse();
+ }
+
+ /**
+ * remove server-to-server share if it was unshared by the owner
+ *
+ * @param int $id ID of the share
+ * @param string|null $token Shared secret between servers
+ * @return Http\DataResponse<Http::STATUS_OK, list<empty>, array{}>
+ * @throws OCSException
+ *
+ * 200: Share unshared successfully
+ */
+ #[NoCSRFRequired]
+ #[PublicPage]
+ public function unshare(int $id, ?string $token = null) {
+ if (!$this->isS2SEnabled()) {
+ throw new OCSException('Server does not support federated cloud sharing', 503);
+ }
+
+ try {
+ $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file');
+ $notification = ['sharedSecret' => $token];
+ $provider->notificationReceived('SHARE_UNSHARED', $id, $notification);
+ $this->eventDispatcher->dispatchTyped(new CriticalActionPerformedEvent('Federated share with id "%s" was unshared', [$id]));
+ } catch (\Exception $e) {
+ $this->logger->debug('processing unshare notification failed: ' . $e->getMessage(), ['exception' => $e]);
+ }
+
+ return new DataResponse();
+ }
+
+ private function cleanupRemote($remote) {
+ $remote = substr($remote, strpos($remote, '://') + 3);
+
+ return rtrim($remote, '/');
+ }
+
+
+ /**
+ * federated share was revoked, either by the owner or the re-sharer
+ *
+ * @param int $id ID of the share
+ * @param string|null $token Shared secret between servers
+ * @return Http\DataResponse<Http::STATUS_OK, list<empty>, array{}>
+ * @throws OCSBadRequestException Revoking the share is not possible
+ *
+ * 200: Share revoked successfully
+ */
+ #[NoCSRFRequired]
+ #[PublicPage]
+ public function revoke(int $id, ?string $token = null) {
+ try {
+ $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file');
+ $notification = ['sharedSecret' => $token];
+ $provider->notificationReceived('RESHARE_UNDO', $id, $notification);
+ return new DataResponse();
+ } catch (\Exception $e) {
+ throw new OCSBadRequestException();
+ }
+ }
+
+ /**
+ * check if server-to-server sharing is enabled
+ *
+ * @param bool $incoming
+ * @return bool
+ */
+ private function isS2SEnabled($incoming = false) {
+ $result = Server::get(IAppManager::class)->isEnabledForUser('files_sharing');
+
+ if ($incoming) {
+ $result = $result && $this->federatedShareProvider->isIncomingServer2serverShareEnabled();
+ } else {
+ $result = $result && $this->federatedShareProvider->isOutgoingServer2serverShareEnabled();
+ }
+
+ return $result;
+ }
+
+ /**
+ * update share information to keep federated re-shares in sync
+ *
+ * @param int $id ID of the share
+ * @param string|null $token Shared secret between servers
+ * @param int|null $permissions New permissions
+ * @return Http\DataResponse<Http::STATUS_OK, list<empty>, array{}>
+ * @throws OCSBadRequestException Updating permissions is not possible
+ *
+ * 200: Permissions updated successfully
+ */
+ #[NoCSRFRequired]
+ #[PublicPage]
+ public function updatePermissions(int $id, ?string $token = null, ?int $permissions = null) {
+ $ncPermissions = $permissions;
+
+ try {
+ $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file');
+ $ocmPermissions = $this->ncPermissions2ocmPermissions((int)$ncPermissions);
+ $notification = ['sharedSecret' => $token, 'permission' => $ocmPermissions];
+ $provider->notificationReceived('RESHARE_CHANGE_PERMISSION', $id, $notification);
+ $this->eventDispatcher->dispatchTyped(new CriticalActionPerformedEvent('Federated share with id "%s" has updated permissions "%s"', [$id, implode(', ', $ocmPermissions)]));
+ } catch (\Exception $e) {
+ $this->logger->debug($e->getMessage(), ['exception' => $e]);
+ throw new OCSBadRequestException();
+ }
+
+ return new DataResponse();
+ }
+
+ /**
+ * translate Nextcloud permissions to OCM Permissions
+ *
+ * @param $ncPermissions
+ * @return array
+ */
+ protected function ncPermissions2ocmPermissions($ncPermissions) {
+ $ocmPermissions = [];
+
+ if ($ncPermissions & Constants::PERMISSION_SHARE) {
+ $ocmPermissions[] = 'share';
+ }
+
+ if ($ncPermissions & Constants::PERMISSION_READ) {
+ $ocmPermissions[] = 'read';
+ }
+
+ if (($ncPermissions & Constants::PERMISSION_CREATE)
+ || ($ncPermissions & Constants::PERMISSION_UPDATE)) {
+ $ocmPermissions[] = 'write';
+ }
+
+ return $ocmPermissions;
+ }
+
+ /**
+ * change the owner of a server-to-server share
+ *
+ * @param int $id ID of the share
+ * @param string|null $token Shared secret between servers
+ * @param string|null $remote Address of the remote
+ * @param string|null $remote_id ID of the remote
+ * @return Http\DataResponse<Http::STATUS_OK, array{remote: string, owner: string}, array{}>
+ * @throws OCSBadRequestException Moving share is not possible
+ *
+ * 200: Share moved successfully
+ */
+ #[NoCSRFRequired]
+ #[PublicPage]
+ public function move(int $id, ?string $token = null, ?string $remote = null, ?string $remote_id = null) {
+ if (!$this->isS2SEnabled()) {
+ throw new OCSException('Server does not support federated cloud sharing', 503);
+ }
+
+ $newRemoteId = (string)($remote_id ?? $id);
+ $cloudId = $this->cloudIdManager->resolveCloudId($remote);
+
+ $qb = $this->connection->getQueryBuilder();
+ $query = $qb->update('share_external')
+ ->set('remote', $qb->createNamedParameter($cloudId->getRemote()))
+ ->set('owner', $qb->createNamedParameter($cloudId->getUser()))
+ ->set('remote_id', $qb->createNamedParameter($newRemoteId))
+ ->where($qb->expr()->eq('remote_id', $qb->createNamedParameter($id)))
+ ->andWhere($qb->expr()->eq('share_token', $qb->createNamedParameter($token)));
+ $affected = $query->executeStatement();
+
+ if ($affected > 0) {
+ return new DataResponse(['remote' => $cloudId->getRemote(), 'owner' => $cloudId->getUser()]);
+ } else {
+ throw new OCSBadRequestException('Share not found or token invalid');
+ }
+ }
+}