diff options
Diffstat (limited to 'apps/cloud_federation_api/lib/Controller/RequestHandlerController.php')
-rw-r--r-- | apps/cloud_federation_api/lib/Controller/RequestHandlerController.php | 111 |
1 files changed, 109 insertions, 2 deletions
diff --git a/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php b/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php index cbd66f52382..23fe509c081 100644 --- a/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php +++ b/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php @@ -1,8 +1,10 @@ <?php + /** * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ + namespace OCA\CloudFederationAPI\Controller; use NCU\Federation\ISignedCloudFederationProvider; @@ -15,15 +17,20 @@ use NCU\Security\Signature\IIncomingSignedRequest; use NCU\Security\Signature\ISignatureManager; use OC\OCM\OCMSignatoryManager; use OCA\CloudFederationAPI\Config; +use OCA\CloudFederationAPI\Db\FederatedInviteMapper; +use OCA\CloudFederationAPI\Events\FederatedInviteAcceptedEvent; use OCA\CloudFederationAPI\ResponseDefinitions; use OCA\FederatedFileSharing\AddressHandler; use OCP\AppFramework\Controller; +use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\BruteForceProtection; use OCP\AppFramework\Http\Attribute\NoCSRFRequired; use OCP\AppFramework\Http\Attribute\OpenAPI; use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\JSONResponse; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\EventDispatcher\IEventDispatcher; use OCP\Federation\Exceptions\ActionNotSupportedException; use OCP\Federation\Exceptions\AuthenticationFailedException; use OCP\Federation\Exceptions\BadRequestException; @@ -61,12 +68,15 @@ class RequestHandlerController extends Controller { private IURLGenerator $urlGenerator, private ICloudFederationProviderManager $cloudFederationProviderManager, private Config $config, + private IEventDispatcher $dispatcher, + private FederatedInviteMapper $federatedInviteMapper, private readonly AddressHandler $addressHandler, private readonly IAppConfig $appConfig, private ICloudFederationFactory $factory, private ICloudIdManager $cloudIdManager, private readonly ISignatureManager $signatureManager, private readonly OCMSignatoryManager $signatoryManager, + private ITimeFactory $timeFactory, ) { parent::__construct($appName, $request); } @@ -107,7 +117,8 @@ class RequestHandlerController extends Controller { } // check if all required parameters are set - if ($shareWith === null || + if ( + $shareWith === null || $name === null || $providerId === null || $resourceType === null || @@ -214,6 +225,101 @@ class RequestHandlerController extends Controller { } /** + * Inform the sender that an invitation was accepted to start sharing + * + * Inform about an accepted invitation so the user on the sender provider's side + * can initiate the OCM share creation. To protect the identity of the parties, + * for shares created following an OCM invitation, the user id MAY be hashed, + * and recipients implementing the OCM invitation workflow MAY refuse to process + * shares coming from unknown parties. + * @link https://cs3org.github.io/OCM-API/docs.html?branch=v1.1.0&repo=OCM-API&user=cs3org#/paths/~1invite-accepted/post + * + * @param string $recipientProvider The address of the recipent's provider + * @param string $token The token used for the invitation + * @param string $userId The userId of the recipient at the recipient's provider + * @param string $email The email address of the recipient + * @param string $name The display name of the recipient + * + * @return JSONResponse<Http::STATUS_OK, array{userID: string, email: string, name: string}, array{}>|JSONResponse<Http::STATUS_FORBIDDEN|Http::STATUS_BAD_REQUEST|Http::STATUS_CONFLICT, array{message: string, error: true}, array{}> + * + * Note: Not implementing 404 Invitation token does not exist, instead using 400 + * 200: Invitation accepted + * 400: Invalid token + * 403: Invitation token does not exist + * 409: User is already known by the OCM provider + */ + #[PublicPage] + #[NoCSRFRequired] + #[BruteForceProtection(action: 'inviteAccepted')] + public function inviteAccepted(string $recipientProvider, string $token, string $userId, string $email, string $name): JSONResponse { + $this->logger->debug('Processing share invitation for ' . $userId . ' with token ' . $token . ' and email ' . $email . ' and name ' . $name); + + $updated = $this->timeFactory->getTime(); + + if ($token === '') { + $response = new JSONResponse(['message' => 'Invalid or non existing token', 'error' => true], Http::STATUS_BAD_REQUEST); + $response->throttle(); + return $response; + } + + try { + $invitation = $this->federatedInviteMapper->findByToken($token); + } catch (DoesNotExistException) { + $response = ['message' => 'Invalid or non existing token', 'error' => true]; + $status = Http::STATUS_BAD_REQUEST; + $response = new JSONResponse($response, $status); + $response->throttle(); + return $response; + } + + if ($invitation->isAccepted() === true) { + $response = ['message' => 'Invite already accepted', 'error' => true]; + $status = Http::STATUS_CONFLICT; + return new JSONResponse($response, $status); + } + + if ($invitation->getExpiredAt() !== null && $updated > $invitation->getExpiredAt()) { + $response = ['message' => 'Invitation expired', 'error' => true]; + $status = Http::STATUS_BAD_REQUEST; + return new JSONResponse($response, $status); + } + $localUser = $this->userManager->get($invitation->getUserId()); + if ($localUser === null) { + $response = ['message' => 'Invalid or non existing token', 'error' => true]; + $status = Http::STATUS_BAD_REQUEST; + $response = new JSONResponse($response, $status); + $response->throttle(); + return $response; + } + + $sharedFromEmail = $localUser->getEMailAddress(); + if ($sharedFromEmail === null) { + $response = ['message' => 'Invalid or non existing token', 'error' => true]; + $status = Http::STATUS_BAD_REQUEST; + $response = new JSONResponse($response, $status); + $response->throttle(); + return $response; + } + $sharedFromDisplayName = $localUser->getDisplayName(); + + $response = ['userID' => $localUser->getUID(), 'email' => $sharedFromEmail, 'name' => $sharedFromDisplayName]; + $status = Http::STATUS_OK; + + $invitation->setAccepted(true); + $invitation->setRecipientEmail($email); + $invitation->setRecipientName($name); + $invitation->setRecipientProvider($recipientProvider); + $invitation->setRecipientUserId($userId); + $invitation->setAcceptedAt($updated); + $invitation = $this->federatedInviteMapper->update($invitation); + + $event = new FederatedInviteAcceptedEvent($invitation); + $this->dispatcher->dispatchTyped($event); + + return new JSONResponse($response, $status); + } + + /** * Send a notification about an existing share * * @param string $notificationType Notification type, e.g. SHARE_ACCEPTED @@ -233,7 +339,8 @@ class RequestHandlerController extends Controller { #[BruteForceProtection(action: 'receiveFederatedShareNotification')] public function receiveNotification($notificationType, $resourceType, $providerId, ?array $notification) { // check if all required parameters are set - if ($notificationType === null || + if ( + $notificationType === null || $resourceType === null || $providerId === null || !is_array($notification) |