RequestHandlerController.php 15 KB

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