From cb64a33db09cb6f4ea1036cce9b45a413b959c7d Mon Sep 17 00:00:00 2001 From: Gary Kim Date: Mon, 14 Jun 2021 20:58:31 -0400 Subject: [PATCH] Implement CloudFederationProvider Signed-off-by: Gary Kim --- appinfo/routes.php | 21 +++ lib/Controller/FederationController.php | 52 +++++++ .../CloudFederationProviderTalk.php | 144 ++++++++++++++++++ lib/Federation/FederationManager.php | 137 +++++++++++++++++ .../Version12000Date20210610232111.php | 28 ++-- lib/Model/Attendee.php | 4 + lib/Model/AttendeeMapper.php | 1 + 7 files changed, 369 insertions(+), 18 deletions(-) create mode 100644 lib/Controller/FederationController.php create mode 100644 lib/Federation/CloudFederationProviderTalk.php create mode 100644 lib/Federation/FederationManager.php diff --git a/appinfo/routes.php b/appinfo/routes.php index 83f76f137cd..17e0f13fd4a 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -505,6 +505,27 @@ ], ], + /** + * Remote + */ + + [ + 'name' => 'Remote#acceptShare', + 'url' => 'api/{apiVersion}/remote/pending/{id}', + 'verb' => 'POST', + 'requirements' => [ + 'apiVersion' => 'v1', + ], + ], + [ + 'name' => 'Remote#rejectShare', + 'url' => 'api/{apiVersion}/remote/pending/{id}', + 'verb' => 'DELETE', + 'requirements' => [ + 'apiVersion' => 'v1', + ], + ], + /** * PublicShareAuth */ diff --git a/lib/Controller/FederationController.php b/lib/Controller/FederationController.php new file mode 100644 index 00000000000..69913fa29cc --- /dev/null +++ b/lib/Controller/FederationController.php @@ -0,0 +1,52 @@ + + * + * @author Gary Kim + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Talk\Controller; + +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCSController; +use OCP\IRequest; + +class FederationController extends OCSController { + public function __construct(string $appName, IRequest $request) { + parent::__construct($appName, $request); + } + + /** + * @param string $id + * @return DataResponse + */ + public function acceptShare(string $id): DataResponse { + + } + + /** + * @param string $id + * @return DataResponse + */ + public function rejectShare(string $id): DataResponse { + + } +} diff --git a/lib/Federation/CloudFederationProviderTalk.php b/lib/Federation/CloudFederationProviderTalk.php new file mode 100644 index 00000000000..04ae75522d7 --- /dev/null +++ b/lib/Federation/CloudFederationProviderTalk.php @@ -0,0 +1,144 @@ + + * + * @author Gary Kim + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Talk\Federation; + +use OC\AppFramework\Http; +use OC\HintException; +use OCA\FederatedFileSharing\AddressHandler; +use OCA\Talk\AppInfo\Application; +use OCA\Talk\Federation\FederationManager; +use OCP\DB\Exception as DBException; +use OCP\Federation\Exceptions\ProviderCouldNotAddShareException; +use OCP\Federation\ICloudFederationProvider; +use OCP\Federation\ICloudFederationShare; +use OCP\IURLGenerator; +use OCP\IUserManager; +use OCP\Notification\IManager as INotificationManager; +use OCP\Share\IShare; + +class CloudFederationProviderTalk implements ICloudFederationProvider { + + /** @var IUserManager */ + private IUserManager $userManager; + + /** @var AddressHandler */ + private AddressHandler $addressHandler; + + /** @var FederationManager */ + private FederationManager $federationManager; + + /** @var INotificationManager */ + private INotificationManager $notificationManager; + + /** @var IURLGenerator */ + private IURLGenerator $urlGenerator; + + public function __construct( + IUserManager $userManager, + AddressHandler $addressHandler, + FederationManager $federationManager, + INotificationManager $notificationManager, + IURLGenerator $urlGenerator + ) { + $this->userManager = $userManager; + $this->addressHandler = $addressHandler; + $this->federationManager = $federationManager; + $this->notificationManager = $notificationManager; + $this->urlGenerator = $urlGenerator; + } + + /** + * @inheritDoc + */ + public function getShareType(): string { + return 'talk-room'; + } + + /** + * @inheritDoc + * @throws HintException + * @throws DBException + */ + public function shareReceived(ICloudFederationShare $share): string { + if (!$this->federationManager->isEnabled()) { + throw new ProviderCouldNotAddShareException('Server does not support talk federation', '', Http::STATUS_SERVICE_UNAVAILABLE); + } + if ($share->getShareType() !== 'user') { + throw new ProviderCouldNotAddShareException('support for sharing with non-groups not implemented yet', '', Http::STATUS_NOT_IMPLEMENTED); + } + + $shareSecret = $share->getShareSecret(); + $shareWith = $share->getShareWith(); + $roomToken = $share->getProviderId(); + [, $remote] = $this->addressHandler->splitUserRemote($share->getOwner()); + + if ($remote && $shareSecret && $shareWith) { + $shareWith = $this->userManager->get($shareWith); + if ($shareWith === null) { + throw new ProviderCouldNotAddShareException('User does not exist', '',Http::STATUS_BAD_REQUEST); + } + + $shareId = $this->federationManager->addRemoteRoom($shareWith, $roomToken, $remote, $shareSecret); + return (string) $shareId; + } + throw new ProviderCouldNotAddShareException('required request data not found', '', Http::STATUS_BAD_REQUEST); + // TODO: Finish implementing shareReceived() method. + } + + /** + * @inheritDoc + */ + public function notificationReceived($notificationType, $providerId, array $notification) { + + // TODO: Implement notificationReceived() method. + } + + private function notifyAboutNewShare(string $shareWith, string $shareId, string $ownerFederatedId, string $sharedByFederatedId, string $name) { + $notification = $this->notificationManager->createNotification(); + $notification->setApp(Application::APP_ID) + ->setUser($shareWith) + ->setDateTime(new \DateTime()); + + $declineAction = $notification->createAction(); + $declineAction->setLabel('decline') + ->setLink($this->urlGenerator->linkToOCSRouteAbsolute('spreed.Remote.rejectShare', ['id' => $shareId]), 'DELETE'); + $notification->addAction($declineAction); + + $acceptAction = $notification->createAction(); + $acceptAction->setLabel('accept') + ->setLink($this->urlGenerator->linkToOCSRouteAbsolute('spreed.Remote.acceptShare', ['id' => $shareId]), 'POST'); + $notification->addAction($acceptAction); + + $this->notificationManager->notify($notification); + } + + /** + * @inheritDoc + */ + public function getSupportedShareTypes() { + return ['user']; + } +} diff --git a/lib/Federation/FederationManager.php b/lib/Federation/FederationManager.php new file mode 100644 index 00000000000..1212769efdb --- /dev/null +++ b/lib/Federation/FederationManager.php @@ -0,0 +1,137 @@ + + * + * @author Gary Kim + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Talk\Federation; + +use OCA\Talk\Exceptions\UnauthorizedException; +use OCA\Talk\Room; +use OCP\DB\Exception as DBException; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\IUser; + +class FederationManager { + /** @var IDBConnection */ + private $db; + /** @var IConfig */ + private $config; + + public function __construct ( + IDBConnection $db, + IConfig $config + ) { + $this->db = $db; + $this->config = $config; + } + + /** + * Determine if Talk federation is enabled on this instance + * @return bool + */ + public function isEnabled(): bool { + // TODO: Set to true once implementation is complete + return $this->config->getSystemValueBool('talk_federation_enabled', false); + } + + /** + * @param IUser $user + * @param string $roomToken + * @param string $remoteUrl + * @param string $sharedSecret + * @return int share id for this specific remote room share + * @throws DBException + */ + public function addRemoteRoom(IUser $user, string $roomToken, string $remoteUrl, string $sharedSecret): int { + $qb = $this->db->getQueryBuilder(); + + $query = $qb->insert('talk_rooms_external') + ->values([ + 'user_id' => $qb->createNamedParameter($user->getUID(), IQueryBuilder::PARAM_STR), + 'room_id' => $qb->createNamedParameter($roomToken, IQueryBuilder::PARAM_STR), + 'remote_url' => $qb->createNamedParameter($remoteUrl, IQueryBuilder::PARAM_STR), + 'shareToken' => $qb->createNamedParameter($sharedSecret, IQueryBuilder::PARAM_STR), + ]) + ->executeQuery(); + + $row = $query->fetch(); + return (int)($row['id']); + } + + /** + * @throws DBException + * @throws UnauthorizedException + */ + public function acceptRemoteRoomShare(IUser $user, int $shareId) { + $share = this->$this->getShare($shareId); + if ($share['user_id'] !== $user->getUID()) { + throw new UnauthorizedException(); + } + + $qb = $this->db->getQueryBuilder(); + + $qb->update('talk_rooms_external') + ->set('accepted', true) + ->where( + $qb->expr()->eq('id', $qb->createNamedParameter($shareId, IQueryBuilder::PARAM_INT)) + ) + ->executeQuery(); + } + + /** + * @throws DBException + * @throws UnauthorizedException + */ + public function rejectRemoteRoomShare(IUser $user, int $shareId) { + $share = this->$this->getShare($shareId); + if ($share['user_id'] !== $user->getUID()) { + throw new UnauthorizedException(); + } + + $qb = $this->db->getQueryBuilder(); + + $qb->delete('talk_rooms_external') + ->where( + $qb->expr()->eq('id', $qb->createNamedParameter($shareId, IQueryBuilder::PARAM_INT)) + ) + ->executeQuery(); + } + + /** + * @throws DBException + */ + public function getShare(int $shareId): array { + $qb = $this->db->getQueryBuilder(); + + $query = $qb->select('*') + ->from('talk_rooms_external') + ->where( + $qb->expr()->eq('id', $qb->createNamedParameter($shareId, IQueryBuilder::PARAM_INT)) + ) + ->executeQuery(); + + return $query->fetch(); + } +} diff --git a/lib/Migration/Version12000Date20210610232111.php b/lib/Migration/Version12000Date20210610232111.php index 01ddb4a759f..7cc33e994db 100644 --- a/lib/Migration/Version12000Date20210610232111.php +++ b/lib/Migration/Version12000Date20210610232111.php @@ -26,6 +26,7 @@ namespace OCA\Talk\Migration; use Closure; +use Doctrine\DBAL\Schema\SchemaException; use Doctrine\DBAL\Types\Types; use OCP\DB\ISchemaWrapper; use OCP\Migration\IOutput; @@ -38,28 +39,19 @@ class Version12000Date20210610232111 extends SimpleMigrationStep { * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` * @param array $options * @return null|ISchemaWrapper + * @throws SchemaException */ - public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) { + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { /** @var ISchemaWrapper $schema */ $schema = $schemaClosure(); - if (!$schema->hasTable('talk_federated_rooms')) { - $table = $schema->createTable('talk_federated_rooms'); - $table->addColumn('id', Types::BIGINT, [ - 'autoincrement' => true, - 'notnull' => true, - ]); - $table->addColumn('room_id', Types::BIGINT, [ - 'notnull' => true, - 'unsigned' => true, - ]); - $table->addColumn('instance_url', Types::TEXT, [ - 'notnull' => true, - ]); - $table->addColumn('password', Types::TEXT, [ - 'notnull' => true, - ]); - $table->setPrimaryKey('id'); + if ($schema->hasTable('talk_attendees')) { + $table = $schema->getTable('talk_attendees'); + if ($table->hasColumn('access_token')) { + $table->addColumn('access_token', Types::STRING, [ + 'notnull' => false, + ]); + } } return $schema; diff --git a/lib/Model/Attendee.php b/lib/Model/Attendee.php index 7766681cd39..428ac03b683 100644 --- a/lib/Model/Attendee.php +++ b/lib/Model/Attendee.php @@ -49,6 +49,8 @@ * @method int getLastMentionMessage() * @method void setReadPrivacy(int $readPrivacy) * @method int getReadPrivacy() + * @method void setAccessToken() + * @method null|string getAccessToken() */ class Attendee extends Entity { public const ACTOR_USERS = 'users'; @@ -106,6 +108,7 @@ public function __construct() { $this->addType('lastReadMessage', 'int'); $this->addType('lastMentionMessage', 'int'); $this->addType('readPrivacy', 'int'); + $this->addType('accessToken', 'string'); } public function getDisplayName(): string { @@ -130,6 +133,7 @@ public function asArray(): array { 'last_read_message' => $this->getLastReadMessage(), 'last_mention_message' => $this->getLastMentionMessage(), 'read_privacy' => $this->getReadPrivacy(), + 'access_token' => $this->getAccessToken(), ]; } } diff --git a/lib/Model/AttendeeMapper.php b/lib/Model/AttendeeMapper.php index 1b928fc9310..724094a09d9 100644 --- a/lib/Model/AttendeeMapper.php +++ b/lib/Model/AttendeeMapper.php @@ -152,6 +152,7 @@ public function createAttendeeFromRow(array $row): Attendee { 'last_read_message' => (int) $row['last_read_message'], 'last_mention_message' => (int) $row['last_mention_message'], 'read_privacy' => (int) $row['read_privacy'], + 'access_token' => (string) $row['access_token'], ]); } }