From 3c4f1001cf2895f12187db533bba9d4863509d88 Mon Sep 17 00:00:00 2001 From: Benjamin Frueh Date: Fri, 3 Jul 2026 16:32:30 +0200 Subject: [PATCH 1/6] feat(federation): schema, outgoing share and frontend support Signed-off-by: Benjamin Frueh --- lib/AppInfo/Application.php | 13 +++ lib/Constants/ShareReceiverType.php | 1 + lib/Db/Table.php | 12 +++ lib/Federation/TablesFederationProvider.php | 31 ++++++ .../Version002003Date20260630000000.php | 39 +++++++ lib/Service/ShareService.php | 48 +++++++++ lib/Service/ValueObject/ShareCreate.php | 4 + src/modules/sidebar/mixins/shareAPI.js | 8 +- src/modules/sidebar/partials/ShareForm.vue | 102 +++++++++--------- src/modules/sidebar/sections/Sidebar.vue | 6 +- src/shared/mixins/searchUserGroup.js | 2 + 11 files changed, 211 insertions(+), 55 deletions(-) create mode 100644 lib/Federation/TablesFederationProvider.php create mode 100644 lib/Migration/Version002003Date20260630000000.php diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 76c2c2a45c..72678f981d 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -15,6 +15,7 @@ use OCA\Tables\Event\TableDeletedEvent; use OCA\Tables\Event\TableOwnershipTransferredEvent; use OCA\Tables\Event\ViewDeletedEvent; +use OCA\Tables\Federation\TablesFederationProvider; use OCA\Tables\Listener\AddMissingIndicesListener; use OCA\Tables\Listener\AnalyticsDatasourceListener; use OCA\Tables\Listener\BeforeTemplateRenderedListener; @@ -42,7 +43,10 @@ use OCP\Collaboration\Reference\RenderReferenceEvent; use OCP\Collaboration\Resources\LoadAdditionalScriptsEvent; use OCP\DB\Events\AddMissingIndicesEvent; +use OCP\Federation\ICloudFederationProvider; +use OCP\Federation\ICloudFederationProviderManager; use OCP\Group\Events\GroupDeletedEvent; +use OCP\Server; use OCP\User\Events\BeforeUserDeletedEvent; use OCP\User\Events\UserDeletedEvent; use Psr\Container\ContainerInterface; @@ -109,5 +113,14 @@ public function register(IRegistrationContext $context): void { } public function boot(IBootContext $context): void { + $context->injectFn([$this, 'registerCloudFederationProviderManager']); + } + + public function registerCloudFederationProviderManager(ICloudFederationProviderManager $manager): void { + $manager->addCloudFederationProvider( + TablesFederationProvider::PROVIDER_ID, + 'Tables Federation', + static fn (): ICloudFederationProvider => Server::get(TablesFederationProvider::class), + ); } } diff --git a/lib/Constants/ShareReceiverType.php b/lib/Constants/ShareReceiverType.php index 558bf5cb64..e09f492d0e 100644 --- a/lib/Constants/ShareReceiverType.php +++ b/lib/Constants/ShareReceiverType.php @@ -15,4 +15,5 @@ class ShareReceiverType { public const GROUP = 'group'; public const CIRCLE = 'circle'; public const LINK = 'link'; + public const REMOTE = 'remote'; } diff --git a/lib/Db/Table.php b/lib/Db/Table.php index 418ce443d2..aec5c289fd 100644 --- a/lib/Db/Table.php +++ b/lib/Db/Table.php @@ -60,6 +60,10 @@ * @method setLastEditBy(string $lastEditBy) * @method getLastEditAt(): string * @method setLastEditAt(string $lastEditAt) + * @method getExternalId(): ?int + * @method setExternalId(?int $externalId) + * @method getShareToken(): ?string + * @method setShareToken(?string $shareToken) */ class Table extends EntitySuper implements JsonSerializable { protected ?string $title = null; @@ -75,6 +79,9 @@ class Table extends EntitySuper implements JsonSerializable { protected ?string $columnOrder = null; // json protected ?string $sort = null; // json + protected ?int $externalId = null; + protected ?string $shareToken = null; + // virtual properties protected ?bool $isShared = null; protected ?Permissions $onSharePermissions = null; @@ -91,6 +98,7 @@ class Table extends EntitySuper implements JsonSerializable { public function __construct() { $this->addType('id', 'integer'); $this->addType('archived', 'boolean'); + $this->addType('externalId', 'integer'); } /** @@ -181,4 +189,8 @@ private function getArray(?string $json): array { } return []; } + + public function isFederated(): bool { + return $this->externalId !== null; + } } diff --git a/lib/Federation/TablesFederationProvider.php b/lib/Federation/TablesFederationProvider.php new file mode 100644 index 0000000000..c61bc7cd45 --- /dev/null +++ b/lib/Federation/TablesFederationProvider.php @@ -0,0 +1,31 @@ +getTable('tables_tables'); + + if (!$table->hasColumn('external_id')) { + $table->addColumn('external_id', Types::INTEGER, [ + 'notnull' => false, + ]); + } + + if (!$table->hasColumn('share_token')) { + $table->addColumn('share_token', Types::STRING, [ + 'notnull' => false, + 'length' => 64, + ]); + } + + return $schema; + } +} diff --git a/lib/Service/ShareService.php b/lib/Service/ShareService.php index 72f8a09524..ce649afab7 100644 --- a/lib/Service/ShareService.php +++ b/lib/Service/ShareService.php @@ -26,6 +26,7 @@ use OCA\Tables\Errors\InternalError; use OCA\Tables\Errors\NotFoundError; use OCA\Tables\Errors\PermissionError; +use OCA\Tables\Federation\TablesFederationProvider; use OCA\Tables\Helper\CircleHelper; use OCA\Tables\Helper\ConversionHelper; use OCA\Tables\Helper\GroupHelper; @@ -38,9 +39,13 @@ use OCP\AppFramework\Db\MultipleObjectsReturnedException; use OCP\AppFramework\Db\TTransactional; use OCP\DB\Exception; +use OCP\Federation\ICloudFederationFactory; +use OCP\Federation\ICloudFederationProviderManager; +use OCP\Federation\ICloudIdManager; use OCP\IDBConnection; use OCP\IGroup; use OCP\IUserManager; +use OCP\OCM\Exceptions\OCMProviderException; use OCP\Security\IHasher; use OCP\Security\ISecureRandom; use OCP\Share\IManager as IShareManager; @@ -69,6 +74,9 @@ public function __construct( private readonly IUserManager $userManager, private readonly IHasher $hasher, private readonly IShareManager $shareManager, + private readonly ICloudFederationFactory $federationFactory, + private readonly ICloudFederationProviderManager $federationProviderManager, + private readonly ICloudIdManager $cloudIdManager, ) { parent::__construct($logger, $userId, $permissionsService); } @@ -297,6 +305,10 @@ public function create(ShareCreate $dto): Share { ); } + if ($dto->getReceiverType() === ShareReceiverType::REMOTE) { + $dto->setShareToken($this->generateShareToken()); + } + return $this->createNodeShare( $dto->getNodeId(), $dto->getNodeType(), @@ -403,6 +415,10 @@ private function createNodeShare( throw new InternalError($e->getMessage()); } + if ($receiverType === ShareReceiverType::REMOTE) { + $this->sendFederatedShare($newShare); + } + return $this->addReceiverDisplayName($newShare); } @@ -456,6 +472,10 @@ private function enforceGroupMembersOnlyPolicy(string $sender, string $receiverT return; } + if ($receiverType === ShareReceiverType::REMOTE) { + throw new PermissionError('Federated sharing is not allowed when sharing is restricted to members of your groups.'); + } + $senderGroupIds = $this->userHelper->getGroupIdsForUser($sender) ?? []; $excludedGroups = $this->shareManager->shareWithGroupMembersOnlyExcludeGroupsList(); if (count(array_intersect($senderGroupIds, $excludedGroups)) > 0) { @@ -682,6 +702,8 @@ private function addReceiverDisplayName(Share $share):Share { ); $share->setReceiverDisplayName($share->getReceiver()); } + } elseif ($share->getReceiverType() === ShareReceiverType::REMOTE) { + $share->setReceiverDisplayName($share->getReceiver()); } else { $this->logger->info('can not use receiver type to get display name'); $share->setReceiverDisplayName($share->getReceiver()); @@ -869,4 +891,30 @@ public function importShare(int $nodeId, array $share, string $userId): void { } } + private function sendFederatedShare(Share $share): void { + $cloudId = $this->cloudIdManager->resolveCloudId($share->getReceiver()); + $ownerCloudId = $this->cloudIdManager->getCloudId($share->getSender(), null); + $ownerDisplayName = $this->userHelper->getUserDisplayName($share->getSender()); + $federationShare = $this->federationFactory->getCloudFederationShare( + $cloudId->getId(), + (string)$share->getNodeId(), + '', + (string)$share->getNodeId(), + $ownerCloudId->getId(), + $ownerDisplayName, + $ownerCloudId->getId(), + $ownerDisplayName, + $share->getToken(), + 'user', + TablesFederationProvider::PROVIDER_ID, + ); + + try { + $this->federationProviderManager->sendCloudShare($federationShare); + } catch (OCMProviderException $e) { + $this->logger->error('Failed to send federated share: ' . $e->getMessage(), ['exception' => $e]); + throw new InternalError('Could not send federated share to remote instance'); + } + } + } diff --git a/lib/Service/ValueObject/ShareCreate.php b/lib/Service/ValueObject/ShareCreate.php index 77f870d829..b38bc22bfe 100644 --- a/lib/Service/ValueObject/ShareCreate.php +++ b/lib/Service/ValueObject/ShareCreate.php @@ -73,4 +73,8 @@ public function getPassword(): ?string { public function getShareToken(): ?ShareToken { return $this->shareToken; } + + public function setShareToken(ShareToken $token): void { + $this->shareToken = $token; + } } diff --git a/src/modules/sidebar/mixins/shareAPI.js b/src/modules/sidebar/mixins/shareAPI.js index ce2c0428a4..23e8a68c9e 100644 --- a/src/modules/sidebar/mixins/shareAPI.js +++ b/src/modules/sidebar/mixins/shareAPI.js @@ -63,10 +63,13 @@ export default { return false } + const isRemote = share.shareType === this.SHARE_TYPES.SHARE_TYPE_REMOTE + const receiver = isRemote ? share.shareWith : share.user + const data = { nodeType: this.isView ? 'view' : 'table', nodeId: this.activeElement.id, - receiver: share.user, + receiver, receiverType: this.getReceiverType(share.shareType), permissionRead: true, permissionCreate: true, @@ -155,6 +158,7 @@ export default { this.SHARE_TYPES.SHARE_TYPE_GROUP, this.SHARE_TYPES.SHARE_TYPE_LINK, ...(this.isCirclesEnabled ? [this.SHARE_TYPES.SHARE_TYPE_CIRCLE] : []), + this.SHARE_TYPES.SHARE_TYPE_REMOTE, ].includes(shareType) }, @@ -166,6 +170,8 @@ export default { return 'group' case this.SHARE_TYPES.SHARE_TYPE_CIRCLE: return 'circle' + case this.SHARE_TYPES.SHARE_TYPE_REMOTE: + return 'remote' default: throw new Error('Invalid share type') } diff --git a/src/modules/sidebar/partials/ShareForm.vue b/src/modules/sidebar/partials/ShareForm.vue index 7cec3de156..c65fdeb825 100644 --- a/src/modules/sidebar/partials/ShareForm.vue +++ b/src/modules/sidebar/partials/ShareForm.vue @@ -5,25 +5,22 @@