diff --git a/appinfo/routes.php b/appinfo/routes.php index 91b835234..64a9738ef 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -118,7 +118,8 @@ ['name' => 'Sync#syncItem', 'url' => '/sync/item', 'verb' => 'GET'], ['name' => 'Sync#updateSyncedItem', 'url' => '/sync/item', 'verb' => 'PUT'], // ['name' => 'Remote#syncItem', 'url' => '/sync/item/{singleId}', 'verb' => 'GET'], - ['name' => 'Sync#syncShare', 'url' => '/sync/share', 'verb' => 'POST'], + ['name' => 'Sync#syncShare', 'url' => '/sync/share', 'verb' => 'GET'], + ['name' => 'Sync#createSyncedShare', 'url' => '/sync/share', 'verb' => 'PUT'], ['name' => 'Debug#debugDaemon', 'url' => '/debug', 'verb' => 'POST'] ] diff --git a/lib/CircleSharesManager.php b/lib/CircleSharesManager.php index 59973b4d9..cc4eae6c8 100644 --- a/lib/CircleSharesManager.php +++ b/lib/CircleSharesManager.php @@ -33,7 +33,6 @@ use Exception; use OCA\Circles\Exceptions\CircleSharesManagerException; -use OCA\Circles\Model\Probes\CircleProbe; use OCA\Circles\Model\SyncedItemLock; use OCA\Circles\Service\CircleService; use OCA\Circles\Service\ConfigService; @@ -145,12 +144,12 @@ public function createShare( try { $this->mustHaveOrigin(); - // TODO: verify rules that apply when sharing to a circle - $probe = new CircleProbe(); - $probe->includeSystemCircles() - ->mustBeMember(); - - $circle = $this->circleService->getCircle($circleId, $probe); + // TODO: do we need this here as we are also checking federatedUser during isShareCreatable() +// $probe = new CircleProbe(); +// $probe->includeSystemCircles() +// ->mustBeMember(); +// +// $circle = $this->circleService->getCircle($circleId, $probe); // get valid SyncedItem based on appId, itemType, itemId $syncedItem = $this->federatedSyncItemService->initSyncedItem( @@ -163,20 +162,19 @@ public function createShare( $this->debugService->info( 'initiating the process of sharing {syncedItem.singleId} to {circle.id}', $circleId, [ - 'circle' => $circle, + 'circleId' => $circleId, 'syncedItem' => $syncedItem, 'extraData' => $extraData, 'isLocal' => $syncedItem->isLocal() ] ); - // confirm item is local - if (!$syncedItem->isLocal()) { - // TODO: sharing a remote item - return; - } - - $this->federatedSyncShareService->createShare($syncedItem, $circle, $extraData); + $this->federatedSyncShareService->requestSyncedShareCreation( + $this->federatedUserService->getCurrentEntity(), + $syncedItem, + $circleId, + $extraData + ); } catch (Exception $e) { $this->debugService->exception($e, $circleId); throw $e; diff --git a/lib/CirclesManager.php b/lib/CirclesManager.php index e375dd8c1..39bea304f 100644 --- a/lib/CirclesManager.php +++ b/lib/CirclesManager.php @@ -59,6 +59,7 @@ use OCA\Circles\Model\Probes\CircleProbe; use OCA\Circles\Service\CircleService; use OCA\Circles\Service\ConfigService; +use OCA\Circles\Service\DebugService; use OCA\Circles\Service\FederatedUserService; use OCA\Circles\Service\MemberService; use OCA\Circles\Service\MembershipService; @@ -74,6 +75,7 @@ class CirclesManager { private MembershipService $membershipService; private ConfigService $configService; private CirclesQueryHelper $circlesQueryHelper; + private DebugService $debugService; /** @@ -94,6 +96,7 @@ public function __construct( MemberService $memberService, MembershipService $membershipService, ConfigService $configService, + DebugService $debugService, CirclesQueryHelper $circlesQueryHelper ) { $this->circleSharesManager = $circleSharesManager; @@ -102,6 +105,7 @@ public function __construct( $this->memberService = $memberService; $this->membershipService = $membershipService; $this->configService = $configService; + $this->debugService = $debugService; $this->circlesQueryHelper = $circlesQueryHelper; } @@ -269,6 +273,12 @@ public function getQueryHelper(): CirclesQueryHelper { return $this->circlesQueryHelper; } + /** + * @return DebugService + */ + public function getDebugService(): DebugService { + return $this->debugService; + } /** * @param string $name diff --git a/lib/Controller/SyncController.php b/lib/Controller/SyncController.php index fd1f82dc6..30b65f304 100644 --- a/lib/Controller/SyncController.php +++ b/lib/Controller/SyncController.php @@ -187,4 +187,68 @@ public function updateSyncedItem(): DataResponse { exit(); } + + /** + * @PublicPage + * @NoCSRFRequired + * + * @return DataResponse + */ + public function createSyncedShare(): DataResponse { + try { + /** @var SyncedWrapper $wrapper */ + $wrapper = $this->signedControllerService->extractObjectFromRequest( + SyncedWrapper::class, + $signed + ); + + $this->debugService->info( + '{instance} is requesting the creation of a new SyncedShare on SyncedItem {syncedWrapper.syncedShare.singleId}', + '', + [ + 'instance' => $signed->getOrigin(), + 'syncedWrapper' => $wrapper + ] + ); + + if (!$wrapper->hasShare() || !$wrapper->hasFederatedUser()) { + throw new FederatedItemBadRequestException(); + } + + $syncedShare = $wrapper->getshare(); + $local = $this->federatedSyncItemService->getLocalSyncedItem($syncedShare->getSingleId()); + + // confirm that remote is in a circle with a share on the item + $this->federatedSyncShareService->confirmRemoteInstanceAccess( + $syncedShare->getSingleId(), + $signed->getOrigin() + ); + + $this->debugService->info( + 'SyncedItem exists, is local, and {instance} have access to the SyncedItem.', '', + [ + 'instance' => $signed->getOrigin(), + 'local' => $local, + ] + ); + + $this->asyncService->setSplittable(true); + $this->federatedSyncShareService->requestSyncedShareCreation( + $wrapper->getFederatedUser(), + $local, + $wrapper->getShare()->getCircleId(), + $wrapper->getExtraData(), + ); + + if (!$this->asyncService->isAsynced()) { + return new DataResponse(['success' => true]); + } + } catch (Exception $e) { + $this->e($e); + + return $this->signedControllerService->exceptionResponse($e, Http::STATUS_UNAUTHORIZED); + } + + exit(); + } } diff --git a/lib/Db/SyncedItemLockRequest.php b/lib/Db/SyncedItemLockRequest.php index 21cbdb24f..6c570e8f5 100644 --- a/lib/Db/SyncedItemLockRequest.php +++ b/lib/Db/SyncedItemLockRequest.php @@ -53,7 +53,7 @@ public function save(SyncedItemLock $lock): void { ->setValue('update_type_id', $qb->createNamedParameter($lock->getUpdateTypeId())) ->setValue('time', $qb->createNamedParameter(time())); - $qb->executeQuery(); + $qb->executeStatement(); } diff --git a/lib/FederatedItems/FederatedSync/ShareCreation.php b/lib/FederatedItems/FederatedSync/ShareCreation.php index 6f2ab4fdd..5f110bcb4 100644 --- a/lib/FederatedItems/FederatedSync/ShareCreation.php +++ b/lib/FederatedItems/FederatedSync/ShareCreation.php @@ -37,9 +37,11 @@ use OCA\Circles\IFederatedItem; use OCA\Circles\IFederatedItemAsyncProcess; use OCA\Circles\IFederatedItemHighSeverity; +use OCA\Circles\IFederatedItemInitiatorCheckNotRequired; use OCA\Circles\IFederatedItemLimitedToInstanceWithMember; use OCA\Circles\IFederatedItemSyncedItem; use OCA\Circles\Model\Federated\FederatedEvent; +use OCA\Circles\Model\FederatedUser; use OCA\Circles\Service\ConfigService; use OCA\Circles\Service\DebugService; use OCA\Circles\Service\FederatedSyncItemService; @@ -52,6 +54,7 @@ class ShareCreation implements IFederatedItemLimitedToInstanceWithMember, IFederatedItemHighSeverity, IFederatedItemAsyncProcess, + IFederatedItemInitiatorCheckNotRequired, IFederatedItemSyncedItem { use TDeserialize; @@ -113,6 +116,21 @@ public function manage(FederatedEvent $event): void { } $circle = $event->getCircle(); + if (!$circle->hasInitiator()) { + $this->debugService->info( + '{?Initiator not available}', '', + ['circle' => $circle] + ); + + return; + } + + // note that we have no confirmation about the origin and/or memberships of FederatedUser at + // this point as this class implements IFederatedItemInitiatorCheckNotRequired + // the only use of this IFederatedItem is to broadcast an eventual new share which needs + // to be confirmed with a direct request to the instance that owns the shared item + $federatedUser = new FederatedUser(); + $federatedUser->importFromIFederatedUser($circle->getInitiator()); $syncedItem = $event->getSyncedItem(); $this->federatedSyncItemService->compareWithKnownItem($syncedItem, true); @@ -120,7 +138,12 @@ public function manage(FederatedEvent $event): void { $extraData = $event->getParams()->gArray('extraData'); $this->federatedSyncItemService->updateSyncedItem($syncedItem); - $this->federatedSyncShareService->syncShareCreation($syncedItem, $circle, $extraData); + $this->federatedSyncShareService->syncShareCreation( + $federatedUser, + $syncedItem, + $circle->getSingleId(), + $extraData + ); } diff --git a/lib/IFederatedSyncManager.php b/lib/IFederatedSyncManager.php index b1a0c9bf6..08ed3d332 100644 --- a/lib/IFederatedSyncManager.php +++ b/lib/IFederatedSyncManager.php @@ -121,6 +121,16 @@ public function syncItem(string $itemId, array $serializedData): void; */ public function itemExists(string $itemId): bool; + /** + * Method is called when Circles needs to know the owner of an item. Must returns a singleId. + * + * @param string $itemId + * + * @return mixed + */ + public function getOwner(string $itemId): string; + + /** * Your app returns details about a share. * Method will be called to re-sync a share on a remote instance @@ -324,5 +334,4 @@ public function onItemModification( IFederatedUser $federatedUser ): void; - } diff --git a/lib/Model/SyncedShare.php b/lib/Model/SyncedShare.php index 8c2e7afef..4d1f8cd0d 100644 --- a/lib/Model/SyncedShare.php +++ b/lib/Model/SyncedShare.php @@ -113,7 +113,8 @@ public function getCircleId(): string { * @throws InvalidItemException */ public function import(array $data): IDeserializable { - if ($this->getInt('singleId', $data) === 0) { + if ($this->get('singleId', $data) === '' + || $this->get('circleId', $data) === '') { throw new InvalidItemException(); } diff --git a/lib/Service/FederatedSyncItemService.php b/lib/Service/FederatedSyncItemService.php index f636b835a..7bf81fab8 100644 --- a/lib/Service/FederatedSyncItemService.php +++ b/lib/Service/FederatedSyncItemService.php @@ -311,7 +311,7 @@ public function requestSyncedItemUpdate( ); return; - } else if (is_null($remoteSum)) { + } else if (is_null($remoteSum)) { // this means that the request is not coming from remote location $this->requestSyncedItemUpdateRemote($federatedUser, $syncedItem, $syncedLock, $extraData); return; @@ -389,7 +389,7 @@ private function requestSyncedItemUpdateRemote( SyncedItem $syncedItem, SyncedItemLock $syncedLock, array $extraData = [] - ): array { + ): void { $wrapper = new SyncedWrapper($federatedUser, $syncedItem, $syncedLock, null, $extraData); $this->interfaceService->setCurrentInterfaceFromInstance($syncedItem->getInstance()); $data = $this->remoteStreamService->resultRequestRemoteInstance( @@ -420,11 +420,9 @@ private function requestSyncedItemUpdateRemote( // update item based on current (old) checksum to confirm item was not already updated (race condition) // do not update checksum // $this->updateChecksum($syncedItem->getSingleId(), $data); - - return $data; } -// -// + + // private function updateChecksum(string $syncedItemId, ?array $data = null): void { // $currSum = ''; // if (is_null($data)) { @@ -579,6 +577,7 @@ private function isItemModifiable( SyncedItemLock $syncedLock, array $extraData = [] ): bool { +// $federatedUser $syncManager = $this->federatedSyncService->initSyncManager($syncedItem); $this->debugService->info( 'sharing of SyncedItem {syncedItem.singleId} looks doable, calling {`isShareCreatable()} on {syncManager.class} for confirmation', diff --git a/lib/Service/FederatedSyncShareService.php b/lib/Service/FederatedSyncShareService.php index 9fa323ca8..1199f0a8e 100644 --- a/lib/Service/FederatedSyncShareService.php +++ b/lib/Service/FederatedSyncShareService.php @@ -30,37 +30,50 @@ namespace OCA\Circles\Service; +use OCA\Circles\Db\CircleRequest; use OCA\Circles\Db\MemberRequest; +use OCA\Circles\Db\MembershipRequest; use OCA\Circles\Db\RemoteRequest; use OCA\Circles\Db\SyncedItemRequest; use OCA\Circles\Db\SyncedShareRequest; +use OCA\Circles\Exceptions\CircleNotFoundException; use OCA\Circles\Exceptions\FederatedEventException; use OCA\Circles\Exceptions\FederatedItemException; +use OCA\Circles\Exceptions\FederatedSyncConflictException; use OCA\Circles\Exceptions\FederatedSyncManagerNotFoundException; +use OCA\Circles\Exceptions\FederatedSyncPermissionException; +use OCA\Circles\Exceptions\FederatedSyncRequestException; use OCA\Circles\Exceptions\InitiatorNotConfirmedException; +use OCA\Circles\Exceptions\InvalidIdException; +use OCA\Circles\Exceptions\MembershipNotFoundException; use OCA\Circles\Exceptions\OwnerNotFoundException; use OCA\Circles\Exceptions\RemoteInstanceException; use OCA\Circles\Exceptions\RemoteNotFoundException; use OCA\Circles\Exceptions\RemoteResourceNotFoundException; use OCA\Circles\Exceptions\RequestBuilderException; -use OCA\Circles\Exceptions\SyncedItemNotFoundException; use OCA\Circles\Exceptions\SyncedSharedAlreadyExistException; use OCA\Circles\Exceptions\SyncedShareNotFoundException; use OCA\Circles\Exceptions\UnknownRemoteException; use OCA\Circles\FederatedItems\FederatedSync\ShareCreation; +use OCA\Circles\IFederatedUser; use OCA\Circles\Model\Circle; use OCA\Circles\Model\Federated\FederatedEvent; -use OCA\Circles\Model\FederatedUser; +use OCA\Circles\Model\Federated\RemoteInstance; +use OCA\Circles\Model\Probes\CircleProbe; use OCA\Circles\Model\SyncedItem; use OCA\Circles\Model\SyncedShare; +use OCA\Circles\Model\SyncedWrapper; use OCA\Circles\Tools\ActivityPub\NCSignature; +use OCA\Circles\Tools\Model\Request; use OCA\Circles\Tools\Model\SimpleDataStore; use OCA\Circles\Tools\Traits\TStringTools; class FederatedSyncShareService extends NCSignature { use TStringTools; + private CircleRequest $circleRequest; private MemberRequest $memberRequest; + private MembershipRequest $membershipRequest; private SyncedItemRequest $syncedItemRequest; private SyncedShareRequest $syncedShareRequest; private RemoteRequest $remoteRequest; @@ -72,6 +85,8 @@ class FederatedSyncShareService extends NCSignature { /** + * @param CircleRequest $circleRequest + * @param MemberRequest $memberRequest * @param SyncedItemRequest $syncedItemRequest * @param SyncedShareRequest $syncedShareRequest * @param RemoteRequest $remoteRequest @@ -82,7 +97,9 @@ class FederatedSyncShareService extends NCSignature { * @param DebugService $debugService */ public function __construct( + CircleRequest $circleRequest, MemberRequest $memberRequest, + MembershipRequest $membershipRequest, SyncedItemRequest $syncedItemRequest, SyncedShareRequest $syncedShareRequest, RemoteRequest $remoteRequest, @@ -92,7 +109,9 @@ public function __construct( InterfaceService $interfaceService, DebugService $debugService ) { + $this->circleRequest = $circleRequest; $this->memberRequest = $memberRequest; + $this->membershipRequest = $membershipRequest; $this->syncedItemRequest = $syncedItemRequest; $this->syncedShareRequest = $syncedShareRequest; $this->remoteRequest = $remoteRequest; @@ -106,27 +125,88 @@ public function __construct( /** * search for existing shares in circles_share based on itemSingleId, circleId. * + * @param IFederatedUser $federatedUser * @param SyncedItem $syncedItem - * @param Circle $circle + * @param string $circleId * @param array $extraData + * @param bool $fromRemote * + * @throws CircleNotFoundException + * @throws FederatedEventException + * @throws FederatedItemException + * @throws FederatedSyncConflictException + * @throws FederatedSyncManagerNotFoundException + * @throws FederatedSyncRequestException + * @throws InitiatorNotConfirmedException + * @throws OwnerNotFoundException + * @throws RemoteInstanceException + * @throws RemoteNotFoundException + * @throws RemoteResourceNotFoundException + * @throws RequestBuilderException * @throws SyncedSharedAlreadyExistException + * @throws UnknownRemoteException + */ + public function requestSyncedShareCreation( + IFederatedUser $federatedUser, + SyncedItem $syncedItem, + string $circleId, + array $extraData = [], + bool $fromRemote = false + ): void { + if ($syncedItem->isLocal()) { + $this->requestSyncedShareCreationLocal($federatedUser, $syncedItem, $circleId, $extraData); + + return; + } else if (!$fromRemote) { + $this->requestSyncedShareCreationRemote($federatedUser, $syncedItem, $circleId, $extraData); + + return; + } + + throw new FederatedSyncConflictException(); + } + + + /** * @throws FederatedSyncManagerNotFoundException + * @throws SyncedSharedAlreadyExistException + * @throws CircleNotFoundException + * @throws RemoteResourceNotFoundException + * @throws RemoteInstanceException + * @throws OwnerNotFoundException + * @throws InitiatorNotConfirmedException + * @throws FederatedItemException + * @throws RequestBuilderException + * @throws FederatedEventException + * @throws RemoteNotFoundException + * @throws UnknownRemoteException */ - public function createShare(SyncedItem $syncedItem, Circle $circle, array $extraData = []) { - if (!$this->isShareCreatable($syncedItem, $circle, $extraData)) { + public function requestSyncedShareCreationLocal( + IFederatedUser $federatedUser, + SyncedItem $syncedItem, + string $circleId, + array $extraData = [] + ): void { + + // TODO: verify rules that apply when sharing to a circle + $probe = new CircleProbe(); + $probe->includeSystemCircles() + ->mustBeMember(); + $circle = $this->circleRequest->getCircle($circleId, $federatedUser, $probe); + + if (!$this->isShareCreatable($federatedUser, $syncedItem, $circleId, $extraData)) { $this->debugService->info( - 'share of SyncedItem {!syncedItem.singleId} to {!circle.id} is set as not creatable by {!syncedItem.appId}', - $circle->getSingleId(), + 'share of SyncedItem {!syncedItem.singleId} to {!circleId} is set as not creatable by {!syncedItem.appId}', + $circleId, [ 'syncedItem' => $syncedItem, - 'circle' => $circle, + 'circleId' => $circleId, 'extraData' => $extraData ] ); } - $this->syncShareCreation($syncedItem, $circle, $extraData); + $this->syncShareCreation($federatedUser, $syncedItem, $circleId, $extraData); $this->broadcastShareCreation($syncedItem, $circle, $extraData); $this->debugService->info( @@ -135,23 +215,59 @@ public function createShare(SyncedItem $syncedItem, Circle $circle, array $extra } + private function requestSyncedShareCreationRemote( + IFederatedUser $federatedUser, + SyncedItem $syncedItem, + string $circleId, + array $extraData = [] + ): void { + $syncedShare = new SyncedShare(); + $syncedShare->setSingleId($syncedItem->getSingleId()) + ->setCircleId($circleId); + + $wrapper = new SyncedWrapper($federatedUser, null, null, $syncedShare, $extraData); + $this->interfaceService->setCurrentInterfaceFromInstance($syncedItem->getInstance()); + $data = $this->remoteStreamService->resultRequestRemoteInstance( + $syncedItem->getInstance(), + RemoteInstance::SYNC_SHARE, + Request::TYPE_PUT, + $wrapper + ); + + if (!$this->getBool('success', $data)) { + throw new FederatedSyncRequestException(); + } + + $syncManager = $this->federatedSyncService->initSyncManager($syncedItem); + $syncManager->onShareCreation( + $syncedItem->getItemId(), + $syncedShare->getCircleId(), + $extraData, + $federatedUser + ); + } + /** + * @param IFederatedUser $federatedUser * @param SyncedItem $syncedItem - * @param Circle $circle + * @param string $circleId * @param array $extraData * * @throws FederatedSyncManagerNotFoundException + * @throws InvalidIdException */ - public function syncShareCreation(SyncedItem $syncedItem, Circle $circle, array $extraData = []): void { + public function syncShareCreation( + IFederatedUser $federatedUser, + SyncedItem $syncedItem, + string $circleId, + array $extraData = [] + ): void { $syncedShare = new SyncedShare(); $syncedShare->setSingleId($syncedItem->getSingleId()) - ->setCircleId($circle->getSingleId()); + ->setCircleId($circleId); $syncManager = $this->federatedSyncService->initSyncManager($syncedItem); - $federatedUser = new FederatedUser(); - $federatedUser->importFromIFederatedUser($circle->getInitiator()); - $this->debugService->info( 'calling {`onShareCreation()} on {syncManager}', $syncedShare->getCircleId(), @@ -160,7 +276,6 @@ public function syncShareCreation(SyncedItem $syncedItem, Circle $circle, array 'syncedItem' => $syncedItem, 'syncedShare' => $syncedShare, 'extraData' => $extraData, - 'initiator' => $circle->getInitiator(), 'federatedUser' => $federatedUser ] ); @@ -175,10 +290,10 @@ public function syncShareCreation(SyncedItem $syncedItem, Circle $circle, array $this->syncedShareRequest->save($syncedShare); $this->debugService->info( 'storing SyncedShare of {syncedShare.singleId} to {syncedShare.circleId} in database', - $circle->getSingleId(), + $circleId, [ 'syncedItem' => $syncedItem, - 'circle' => $circle, + 'circleId' => $circleId, 'syncedShare' => $syncedShare ] ); @@ -229,26 +344,39 @@ private function broadcastShareCreation( * - SyncedShare is not already known * - app agree on creating this share * + * @param IFederatedUser $federatedUser * @param SyncedItem $syncedItem - * @param Circle $circle + * @param string $circleId * @param array $extraData * * @return bool * @throws FederatedSyncManagerNotFoundException + * @throws FederatedSyncPermissionException * @throws SyncedSharedAlreadyExistException */ - private function isShareCreatable(SyncedItem $syncedItem, Circle $circle, array $extraData = []): bool { + private function isShareCreatable( + IFederatedUser $federatedUser, + SyncedItem $syncedItem, + string $circleId, + array $extraData = [] + ): bool { try { - $this->syncedShareRequest->getShare($syncedItem->getSingleId(), $circle->getsingleId()); + $this->syncedShareRequest->getShare($syncedItem->getSingleId(), $circleId); throw new SyncedSharedAlreadyExistException('share already exists'); } catch (SyncedShareNotFoundException $e) { } - $syncManager = $this->federatedSyncService->initSyncManager($syncedItem); + $ownerId = $syncManager->getOwner($syncedItem->getItemId()); + try { + $member = $this->membershipRequest->getMembership($circleId, $ownerId); + } catch (MembershipNotFoundException $e) { + throw new FederatedSyncPermissionException('owner of Item is not member of Circle'); + } + $this->debugService->info( 'sharing of SyncedItem {syncedItem.singleId} looks doable, calling {`isShareCreatable()} on {syncManager.class} for confirmation', - $circle->getSingleId(), + $circleId, [ 'syncedItem' => $syncedItem, 'syncManager' => ['class' => get_class($syncManager)] @@ -257,9 +385,9 @@ private function isShareCreatable(SyncedItem $syncedItem, Circle $circle, array return $syncManager->isShareCreatable( $syncedItem->getItemId(), - $circle->getSingleId(), + $circleId, $extraData, - $circle->getInitiator()->getInheritedBy() + $federatedUser ); }