RequestHandlerController.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
  5. * SPDX-License-Identifier: AGPL-3.0-only
  6. */
  7. namespace OCA\FederatedFileSharing\Controller;
  8. use OCA\FederatedFileSharing\AddressHandler;
  9. use OCA\FederatedFileSharing\FederatedShareProvider;
  10. use OCA\FederatedFileSharing\Notifications;
  11. use OCP\App\IAppManager;
  12. use OCP\AppFramework\Http;
  13. use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
  14. use OCP\AppFramework\Http\Attribute\OpenAPI;
  15. use OCP\AppFramework\Http\Attribute\PublicPage;
  16. use OCP\AppFramework\Http\DataResponse;
  17. use OCP\AppFramework\OCS\OCSBadRequestException;
  18. use OCP\AppFramework\OCS\OCSException;
  19. use OCP\AppFramework\OCSController;
  20. use OCP\Constants;
  21. use OCP\EventDispatcher\IEventDispatcher;
  22. use OCP\Federation\Exceptions\ProviderCouldNotAddShareException;
  23. use OCP\Federation\Exceptions\ProviderDoesNotExistsException;
  24. use OCP\Federation\ICloudFederationFactory;
  25. use OCP\Federation\ICloudFederationProviderManager;
  26. use OCP\Federation\ICloudIdManager;
  27. use OCP\HintException;
  28. use OCP\IDBConnection;
  29. use OCP\IRequest;
  30. use OCP\IUserManager;
  31. use OCP\Log\Audit\CriticalActionPerformedEvent;
  32. use OCP\Server;
  33. use OCP\Share;
  34. use OCP\Share\Exceptions\ShareNotFound;
  35. use Psr\Log\LoggerInterface;
  36. #[OpenAPI(scope: OpenAPI::SCOPE_FEDERATION)]
  37. class RequestHandlerController extends OCSController {
  38. /** @var string */
  39. private $shareTable = 'share';
  40. public function __construct(
  41. string $appName,
  42. IRequest $request,
  43. private FederatedShareProvider $federatedShareProvider,
  44. private IDBConnection $connection,
  45. private Share\IManager $shareManager,
  46. private Notifications $notifications,
  47. private AddressHandler $addressHandler,
  48. private IUserManager $userManager,
  49. private ICloudIdManager $cloudIdManager,
  50. private LoggerInterface $logger,
  51. private ICloudFederationFactory $cloudFederationFactory,
  52. private ICloudFederationProviderManager $cloudFederationProviderManager,
  53. private IEventDispatcher $eventDispatcher,
  54. ) {
  55. parent::__construct($appName, $request);
  56. }
  57. /**
  58. * create a new share
  59. *
  60. * @param string|null $remote Address of the remote
  61. * @param string|null $token Shared secret between servers
  62. * @param string|null $name Name of the shared resource
  63. * @param string|null $owner Display name of the receiver
  64. * @param string|null $sharedBy Display name of the sender
  65. * @param string|null $shareWith ID of the user that receives the share
  66. * @param int|null $remoteId ID of the remote
  67. * @param string|null $sharedByFederatedId Federated ID of the sender
  68. * @param string|null $ownerFederatedId Federated ID of the receiver
  69. * @return Http\DataResponse<Http::STATUS_OK, list<empty>, array{}>
  70. * @throws OCSException
  71. *
  72. * 200: Share created successfully
  73. */
  74. #[NoCSRFRequired]
  75. #[PublicPage]
  76. public function createShare(
  77. ?string $remote = null,
  78. ?string $token = null,
  79. ?string $name = null,
  80. ?string $owner = null,
  81. ?string $sharedBy = null,
  82. ?string $shareWith = null,
  83. ?int $remoteId = null,
  84. ?string $sharedByFederatedId = null,
  85. ?string $ownerFederatedId = null,
  86. ) {
  87. if ($ownerFederatedId === null) {
  88. $ownerFederatedId = $this->cloudIdManager->getCloudId($owner, $this->cleanupRemote($remote))->getId();
  89. }
  90. // if the owner of the share and the initiator are the same user
  91. // we also complete the federated share ID for the initiator
  92. if ($sharedByFederatedId === null && $owner === $sharedBy) {
  93. $sharedByFederatedId = $ownerFederatedId;
  94. }
  95. $share = $this->cloudFederationFactory->getCloudFederationShare(
  96. $shareWith,
  97. $name,
  98. '',
  99. $remoteId,
  100. $ownerFederatedId,
  101. $owner,
  102. $sharedByFederatedId,
  103. $sharedBy,
  104. $token,
  105. 'user',
  106. 'file'
  107. );
  108. try {
  109. $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file');
  110. $provider->shareReceived($share);
  111. if ($sharedByFederatedId === $ownerFederatedId) {
  112. $this->eventDispatcher->dispatchTyped(new CriticalActionPerformedEvent('A new federated share with "%s" was created by "%s" and shared with "%s"', [$name, $ownerFederatedId, $shareWith]));
  113. } else {
  114. $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]));
  115. }
  116. } catch (ProviderDoesNotExistsException $e) {
  117. throw new OCSException('Server does not support federated cloud sharing', 503);
  118. } catch (ProviderCouldNotAddShareException $e) {
  119. throw new OCSException($e->getMessage(), 400);
  120. } catch (\Exception $e) {
  121. throw new OCSException('internal server error, was not able to add share from ' . $remote, 500);
  122. }
  123. return new DataResponse();
  124. }
  125. /**
  126. * create re-share on behalf of another user
  127. *
  128. * @param int $id ID of the share
  129. * @param string|null $token Shared secret between servers
  130. * @param string|null $shareWith ID of the user that receives the share
  131. * @param int|null $remoteId ID of the remote
  132. * @return Http\DataResponse<Http::STATUS_OK, array{token: string, remoteId: string}, array{}>
  133. * @throws OCSBadRequestException Re-sharing is not possible
  134. * @throws OCSException
  135. *
  136. * 200: Remote share returned
  137. */
  138. #[NoCSRFRequired]
  139. #[PublicPage]
  140. public function reShare(int $id, ?string $token = null, ?string $shareWith = null, ?int $remoteId = 0) {
  141. if ($token === null ||
  142. $shareWith === null ||
  143. $remoteId === null
  144. ) {
  145. throw new OCSBadRequestException();
  146. }
  147. $notification = [
  148. 'sharedSecret' => $token,
  149. 'shareWith' => $shareWith,
  150. 'senderId' => $remoteId,
  151. 'message' => 'Recipient of a share ask the owner to reshare the file'
  152. ];
  153. try {
  154. $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file');
  155. [$newToken, $localId] = $provider->notificationReceived('REQUEST_RESHARE', $id, $notification);
  156. return new DataResponse([
  157. 'token' => $newToken,
  158. 'remoteId' => $localId
  159. ]);
  160. } catch (ProviderDoesNotExistsException $e) {
  161. throw new OCSException('Server does not support federated cloud sharing', 503);
  162. } catch (ShareNotFound $e) {
  163. $this->logger->debug('Share not found: ' . $e->getMessage(), ['exception' => $e]);
  164. } catch (\Exception $e) {
  165. $this->logger->debug('internal server error, can not process notification: ' . $e->getMessage(), ['exception' => $e]);
  166. }
  167. throw new OCSBadRequestException();
  168. }
  169. /**
  170. * accept server-to-server share
  171. *
  172. * @param int $id ID of the remote share
  173. * @param string|null $token Shared secret between servers
  174. * @return Http\DataResponse<Http::STATUS_OK, list<empty>, array{}>
  175. * @throws OCSException
  176. * @throws ShareNotFound
  177. * @throws HintException
  178. *
  179. * 200: Share accepted successfully
  180. */
  181. #[NoCSRFRequired]
  182. #[PublicPage]
  183. public function acceptShare(int $id, ?string $token = null) {
  184. $notification = [
  185. 'sharedSecret' => $token,
  186. 'message' => 'Recipient accept the share'
  187. ];
  188. try {
  189. $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file');
  190. $provider->notificationReceived('SHARE_ACCEPTED', $id, $notification);
  191. $this->eventDispatcher->dispatchTyped(new CriticalActionPerformedEvent('Federated share with id "%s" was accepted', [$id]));
  192. } catch (ProviderDoesNotExistsException $e) {
  193. throw new OCSException('Server does not support federated cloud sharing', 503);
  194. } catch (ShareNotFound $e) {
  195. $this->logger->debug('Share not found: ' . $e->getMessage(), ['exception' => $e]);
  196. } catch (\Exception $e) {
  197. $this->logger->debug('internal server error, can not process notification: ' . $e->getMessage(), ['exception' => $e]);
  198. }
  199. return new DataResponse();
  200. }
  201. /**
  202. * decline server-to-server share
  203. *
  204. * @param int $id ID of the remote share
  205. * @param string|null $token Shared secret between servers
  206. * @return Http\DataResponse<Http::STATUS_OK, list<empty>, array{}>
  207. * @throws OCSException
  208. *
  209. * 200: Share declined successfully
  210. */
  211. #[NoCSRFRequired]
  212. #[PublicPage]
  213. public function declineShare(int $id, ?string $token = null) {
  214. $notification = [
  215. 'sharedSecret' => $token,
  216. 'message' => 'Recipient declined the share'
  217. ];
  218. try {
  219. $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file');
  220. $provider->notificationReceived('SHARE_DECLINED', $id, $notification);
  221. $this->eventDispatcher->dispatchTyped(new CriticalActionPerformedEvent('Federated share with id "%s" was declined', [$id]));
  222. } catch (ProviderDoesNotExistsException $e) {
  223. throw new OCSException('Server does not support federated cloud sharing', 503);
  224. } catch (ShareNotFound $e) {
  225. $this->logger->debug('Share not found: ' . $e->getMessage(), ['exception' => $e]);
  226. } catch (\Exception $e) {
  227. $this->logger->debug('internal server error, can not process notification: ' . $e->getMessage(), ['exception' => $e]);
  228. }
  229. return new DataResponse();
  230. }
  231. /**
  232. * remove server-to-server share if it was unshared by the owner
  233. *
  234. * @param int $id ID of the share
  235. * @param string|null $token Shared secret between servers
  236. * @return Http\DataResponse<Http::STATUS_OK, list<empty>, array{}>
  237. * @throws OCSException
  238. *
  239. * 200: Share unshared successfully
  240. */
  241. #[NoCSRFRequired]
  242. #[PublicPage]
  243. public function unshare(int $id, ?string $token = null) {
  244. if (!$this->isS2SEnabled()) {
  245. throw new OCSException('Server does not support federated cloud sharing', 503);
  246. }
  247. try {
  248. $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file');
  249. $notification = ['sharedSecret' => $token];
  250. $provider->notificationReceived('SHARE_UNSHARED', $id, $notification);
  251. $this->eventDispatcher->dispatchTyped(new CriticalActionPerformedEvent('Federated share with id "%s" was unshared', [$id]));
  252. } catch (\Exception $e) {
  253. $this->logger->debug('processing unshare notification failed: ' . $e->getMessage(), ['exception' => $e]);
  254. }
  255. return new DataResponse();
  256. }
  257. private function cleanupRemote($remote) {
  258. $remote = substr($remote, strpos($remote, '://') + 3);
  259. return rtrim($remote, '/');
  260. }
  261. /**
  262. * federated share was revoked, either by the owner or the re-sharer
  263. *
  264. * @param int $id ID of the share
  265. * @param string|null $token Shared secret between servers
  266. * @return Http\DataResponse<Http::STATUS_OK, list<empty>, array{}>
  267. * @throws OCSBadRequestException Revoking the share is not possible
  268. *
  269. * 200: Share revoked successfully
  270. */
  271. #[NoCSRFRequired]
  272. #[PublicPage]
  273. public function revoke(int $id, ?string $token = null) {
  274. try {
  275. $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file');
  276. $notification = ['sharedSecret' => $token];
  277. $provider->notificationReceived('RESHARE_UNDO', $id, $notification);
  278. return new DataResponse();
  279. } catch (\Exception $e) {
  280. throw new OCSBadRequestException();
  281. }
  282. }
  283. /**
  284. * check if server-to-server sharing is enabled
  285. *
  286. * @param bool $incoming
  287. * @return bool
  288. */
  289. private function isS2SEnabled($incoming = false) {
  290. $result = Server::get(IAppManager::class)->isEnabledForUser('files_sharing');
  291. if ($incoming) {
  292. $result = $result && $this->federatedShareProvider->isIncomingServer2serverShareEnabled();
  293. } else {
  294. $result = $result && $this->federatedShareProvider->isOutgoingServer2serverShareEnabled();
  295. }
  296. return $result;
  297. }
  298. /**
  299. * update share information to keep federated re-shares in sync
  300. *
  301. * @param int $id ID of the share
  302. * @param string|null $token Shared secret between servers
  303. * @param int|null $permissions New permissions
  304. * @return Http\DataResponse<Http::STATUS_OK, list<empty>, array{}>
  305. * @throws OCSBadRequestException Updating permissions is not possible
  306. *
  307. * 200: Permissions updated successfully
  308. */
  309. #[NoCSRFRequired]
  310. #[PublicPage]
  311. public function updatePermissions(int $id, ?string $token = null, ?int $permissions = null) {
  312. $ncPermissions = $permissions;
  313. try {
  314. $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file');
  315. $ocmPermissions = $this->ncPermissions2ocmPermissions((int)$ncPermissions);
  316. $notification = ['sharedSecret' => $token, 'permission' => $ocmPermissions];
  317. $provider->notificationReceived('RESHARE_CHANGE_PERMISSION', $id, $notification);
  318. $this->eventDispatcher->dispatchTyped(new CriticalActionPerformedEvent('Federated share with id "%s" has updated permissions "%s"', [$id, implode(', ', $ocmPermissions)]));
  319. } catch (\Exception $e) {
  320. $this->logger->debug($e->getMessage(), ['exception' => $e]);
  321. throw new OCSBadRequestException();
  322. }
  323. return new DataResponse();
  324. }
  325. /**
  326. * translate Nextcloud permissions to OCM Permissions
  327. *
  328. * @param $ncPermissions
  329. * @return array
  330. */
  331. protected function ncPermissions2ocmPermissions($ncPermissions) {
  332. $ocmPermissions = [];
  333. if ($ncPermissions & Constants::PERMISSION_SHARE) {
  334. $ocmPermissions[] = 'share';
  335. }
  336. if ($ncPermissions & Constants::PERMISSION_READ) {
  337. $ocmPermissions[] = 'read';
  338. }
  339. if (($ncPermissions & Constants::PERMISSION_CREATE) ||
  340. ($ncPermissions & Constants::PERMISSION_UPDATE)) {
  341. $ocmPermissions[] = 'write';
  342. }
  343. return $ocmPermissions;
  344. }
  345. /**
  346. * change the owner of a server-to-server share
  347. *
  348. * @param int $id ID of the share
  349. * @param string|null $token Shared secret between servers
  350. * @param string|null $remote Address of the remote
  351. * @param string|null $remote_id ID of the remote
  352. * @return Http\DataResponse<Http::STATUS_OK, array{remote: string, owner: string}, array{}>
  353. * @throws OCSBadRequestException Moving share is not possible
  354. *
  355. * 200: Share moved successfully
  356. */
  357. #[NoCSRFRequired]
  358. #[PublicPage]
  359. public function move(int $id, ?string $token = null, ?string $remote = null, ?string $remote_id = null) {
  360. if (!$this->isS2SEnabled()) {
  361. throw new OCSException('Server does not support federated cloud sharing', 503);
  362. }
  363. $newRemoteId = (string)($remote_id ?? $id);
  364. $cloudId = $this->cloudIdManager->resolveCloudId($remote);
  365. $qb = $this->connection->getQueryBuilder();
  366. $query = $qb->update('share_external')
  367. ->set('remote', $qb->createNamedParameter($cloudId->getRemote()))
  368. ->set('owner', $qb->createNamedParameter($cloudId->getUser()))
  369. ->set('remote_id', $qb->createNamedParameter($newRemoteId))
  370. ->where($qb->expr()->eq('remote_id', $qb->createNamedParameter($id)))
  371. ->andWhere($qb->expr()->eq('share_token', $qb->createNamedParameter($token)));
  372. $affected = $query->executeStatement();
  373. if ($affected > 0) {
  374. return new DataResponse(['remote' => $cloudId->getRemote(), 'owner' => $cloudId->getUser()]);
  375. } else {
  376. throw new OCSBadRequestException('Share not found or token invalid');
  377. }
  378. }
  379. }