diff --git a/lib/Chat/MessageParser.php b/lib/Chat/MessageParser.php index d9a822f7cc1..5b2aaa45d8f 100644 --- a/lib/Chat/MessageParser.php +++ b/lib/Chat/MessageParser.php @@ -112,6 +112,9 @@ protected function setActor(Message $message): void { $displayName = $botName . ' (Bot)'; } } + } elseif ($comment->getActorType() === Attendee::ACTOR_FEDERATED_USERS) { + // FIXME Read from some addressbooks? + $displayName = $actorId; } $message->setActor( diff --git a/lib/Controller/AEnvironmentAwareController.php b/lib/Controller/AEnvironmentAwareController.php index 2110e82b60f..1a96e207b47 100644 --- a/lib/Controller/AEnvironmentAwareController.php +++ b/lib/Controller/AEnvironmentAwareController.php @@ -36,6 +36,8 @@ abstract class AEnvironmentAwareController extends OCSController { protected int $apiVersion = 1; protected ?Room $room = null; protected ?Participant $participant = null; + protected ?string $federationCloudId = null; + protected ?string $federationAccessToken = null; public function setAPIVersion(int $apiVersion): void { $this->apiVersion = $apiVersion; @@ -61,6 +63,19 @@ public function getParticipant(): ?Participant { return $this->participant; } + public function setRemoteAccess(?string $actorId, ?string $accessToken): void { + $this->federationCloudId = $actorId; + $this->federationAccessToken = $accessToken; + } + + public function getRemoteAccessCloudId(): ?string { + return $this->federationCloudId; + } + + public function getRemoteAccessToken(): ?string { + return $this->federationAccessToken; + } + /** * Following the logic of {@see Dispatcher::executeController} * @return string Either 'json' or 'xml' diff --git a/lib/Controller/ChatController.php b/lib/Controller/ChatController.php index 25f6548ecd4..0dcb77c0b97 100644 --- a/lib/Controller/ChatController.php +++ b/lib/Controller/ChatController.php @@ -117,23 +117,27 @@ public function __construct( parent::__construct($appName, $request); } + /** + * @return list{0: Attendee::ACTOR_*, 1: string} + */ protected function getActorInfo(string $actorDisplayName = ''): array { - if ($this->userId === null) { - $actorType = Attendee::ACTOR_GUESTS; - $actorId = $this->participant->getAttendee()->getActorId(); + $remoteCloudId = $this->getRemoteAccessCloudId(); + if ($remoteCloudId !== null) { + return [Attendee::ACTOR_FEDERATED_USERS, $remoteCloudId]; + } + if ($this->userId === null) { if ($actorDisplayName) { $this->guestManager->updateName($this->room, $this->participant, $actorDisplayName); } - } elseif ($this->userId === MatterbridgeManager::BRIDGE_BOT_USERID && $actorDisplayName) { - $actorType = Attendee::ACTOR_BRIDGED; - $actorId = str_replace(["/", "\""], "", $actorDisplayName); - } else { - $actorType = Attendee::ACTOR_USERS; - $actorId = $this->userId; + return [Attendee::ACTOR_GUESTS, $this->participant->getAttendee()->getActorId()]; + } + + if ($this->userId === MatterbridgeManager::BRIDGE_BOT_USERID && $actorDisplayName) { + return [Attendee::ACTOR_BRIDGED, str_replace(['/', '"'], '', $actorDisplayName)]; } - return [$actorType, $actorId]; + return [Attendee::ACTOR_USERS, $this->userId]; } /** diff --git a/lib/Controller/RoomController.php b/lib/Controller/RoomController.php index 977b44a67b0..108d6999c73 100644 --- a/lib/Controller/RoomController.php +++ b/lib/Controller/RoomController.php @@ -306,6 +306,7 @@ public function getBreakoutRooms(): DataResponse { * 404: Room not found */ #[PublicPage] + #[BruteForceProtection(action: 'talkFederationAccess')] #[BruteForceProtection(action: 'talkRoomToken')] #[BruteForceProtection(action: 'talkSipBridgeSecret')] public function getSingleRoom(string $token): DataResponse { @@ -325,18 +326,38 @@ public function getSingleRoom(string $token): DataResponse { $includeLastMessage = !$isSIPBridgeRequest; try { - $sessionId = $this->session->getSessionForRoom($token); - $room = $this->manager->getRoomForUserByToken($token, $this->userId, $sessionId, $includeLastMessage, $isSIPBridgeRequest); - + $action = 'talkRoomToken'; $participant = null; - try { - $participant = $this->participantService->getParticipant($room, $this->userId, $sessionId); - } catch (ParticipantNotFoundException $e) { + + $isTalkFederation = $this->request->getHeader('X-Nextcloud-Federation'); + + if (!$isTalkFederation) { + $sessionId = $this->session->getSessionForRoom($token); + $room = $this->manager->getRoomForUserByToken($token, $this->userId, $sessionId, $includeLastMessage, $isSIPBridgeRequest); + try { - $participant = $this->participantService->getParticipantBySession($room, $sessionId); + $participant = $this->participantService->getParticipant($room, $this->userId, $sessionId); } catch (ParticipantNotFoundException $e) { + try { + $participant = $this->participantService->getParticipantBySession($room, $sessionId); + } catch (ParticipantNotFoundException $e) { + } } + } else { + $action = 'talkFederationAccess'; + $room = $this->manager->getRoomByRemoteAccess( + $token, + Attendee::ACTOR_FEDERATED_USERS, + $this->getRemoteAccessCloudId(), + $this->getRemoteAccessToken(), + ); + $participant = $this->participantService->getParticipantByActor( + $room, + Attendee::ACTOR_FEDERATED_USERS, + $this->getRemoteAccessCloudId(), + ); } + $statuses = []; if ($this->userId !== null && $this->appManager->isEnabledForUser('user_status')) { @@ -362,7 +383,7 @@ public function getSingleRoom(string $token): DataResponse { * @var DataResponse $response */ $response = new DataResponse([], Http::STATUS_NOT_FOUND); - $response->throttle(['token' => $token, 'action' => 'talkRoomToken']); + $response->throttle(['token' => $token, 'action' => $action]); return $response; } } @@ -1341,15 +1362,30 @@ public function setPassword(string $password): DataResponse { * 409: Session already exists */ #[PublicPage] + #[BruteForceProtection(action: 'talkFederationAccess')] #[BruteForceProtection(action: 'talkRoomPassword')] #[BruteForceProtection(action: 'talkRoomToken')] public function joinRoom(string $token, string $password = '', bool $force = true): DataResponse { $sessionId = $this->session->getSessionForRoom($token); + $isTalkFederation = $this->request->getHeader('X-Nextcloud-Federation'); try { // The participant is just joining, so enforce to not load any session - $room = $this->manager->getRoomForUserByToken($token, $this->userId, null); + if (!$isTalkFederation) { + $action = 'talkRoomToken'; + $room = $this->manager->getRoomForUserByToken($token, $this->userId, null); + } else { + $action = 'talkFederationAccess'; + $room = $this->manager->getRoomByRemoteAccess( + $token, + Attendee::ACTOR_FEDERATED_USERS, + $this->getRemoteAccessCloudId(), + $this->getRemoteAccessToken(), + ); + } } catch (RoomNotFoundException $e) { - return new DataResponse([], Http::STATUS_NOT_FOUND); + $response = new DataResponse([], Http::STATUS_NOT_FOUND); + $response->throttle(['token' => $token, 'action' => $action]); + return $response; } /** @var Participant|null $previousSession */ @@ -1392,6 +1428,8 @@ public function joinRoom(string $token, string $password = '', bool $force = tru if ($user instanceof IUser) { $participant = $this->participantService->joinRoom($this->roomService, $room, $user, $password, $result['result']); $this->participantService->generatePinForParticipant($room, $participant); + } elseif ($isTalkFederation) { + $participant = $this->participantService->joinRoomAsFederatedUser($room, Attendee::ACTOR_FEDERATED_USERS, $this->getRemoteAccessCloudId()); } else { $participant = $this->participantService->joinRoomAsNewGuest($this->roomService, $room, $password, $result['result'], $previousParticipant); } @@ -1403,7 +1441,7 @@ public function joinRoom(string $token, string $password = '', bool $force = tru return $response; } catch (UnauthorizedException $e) { $response = new DataResponse([], Http::STATUS_NOT_FOUND); - $response->throttle(['token' => $token, 'action' => 'talkRoomToken']); + $response->throttle(['token' => $token, 'action' => $action]); return $response; } @@ -1523,8 +1561,24 @@ public function leaveRoom(string $token): DataResponse { $this->session->removeSessionForRoom($token); try { - $room = $this->manager->getRoomForUserByToken($token, $this->userId, $sessionId); - $participant = $this->participantService->getParticipantBySession($room, $sessionId); + $isTalkFederation = $this->request->getHeader('X-Nextcloud-Federation'); + // The participant is just joining, so enforce to not load any session + if (!$isTalkFederation) { + $room = $this->manager->getRoomForUserByToken($token, $this->userId, $sessionId); + $participant = $this->participantService->getParticipantBySession($room, $sessionId); + } else { + $room = $this->manager->getRoomByRemoteAccess( + $token, + Attendee::ACTOR_FEDERATED_USERS, + $this->getRemoteAccessCloudId(), + $this->getRemoteAccessToken(), + ); + $participant = $this->participantService->getParticipantByActor( + $room, + Attendee::ACTOR_FEDERATED_USERS, + $this->getRemoteAccessCloudId(), + ); + } $this->participantService->leaveRoomAsSession($room, $participant); } catch (RoomNotFoundException $e) { } catch (ParticipantNotFoundException $e) { diff --git a/lib/Events/BeforeFederatedUserJoinedRoomEvent.php b/lib/Events/BeforeFederatedUserJoinedRoomEvent.php new file mode 100644 index 00000000000..b4df0c1729a --- /dev/null +++ b/lib/Events/BeforeFederatedUserJoinedRoomEvent.php @@ -0,0 +1,35 @@ + + * + * @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\Events; + +class BeforeFederatedUserJoinedRoomEvent extends FederatedUserJoinedRoomEvent { + protected bool $cancelJoin = false; + + public function isJoinCanceled(): bool { + return $this->cancelJoin; + } + public function cancelJoin(): void { + $this->cancelJoin = true; + } +} diff --git a/lib/Events/FederatedUserJoinedRoomEvent.php b/lib/Events/FederatedUserJoinedRoomEvent.php new file mode 100644 index 00000000000..480cf092e42 --- /dev/null +++ b/lib/Events/FederatedUserJoinedRoomEvent.php @@ -0,0 +1,39 @@ + + * + * @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\Events; + +use OCA\Talk\Room; + +class FederatedUserJoinedRoomEvent extends RoomEvent { + public function __construct( + Room $room, + protected string $cloudId, + ) { + parent::__construct($room); + } + + public function getCloudId(): string { + return $this->cloudId; + } +} diff --git a/lib/Federation/FederationManager.php b/lib/Federation/FederationManager.php index fb41e5317f5..55acd2f5a99 100644 --- a/lib/Federation/FederationManager.php +++ b/lib/Federation/FederationManager.php @@ -51,7 +51,7 @@ class FederationManager { public const TALK_ROOM_RESOURCE = 'talk-room'; public const TALK_PROTOCOL_NAME = 'nctalk'; - public const TOKEN_LENGTH = 15; + public const TOKEN_LENGTH = 64; public function __construct( private IConfig $config, diff --git a/lib/Manager.php b/lib/Manager.php index fc6f8b46465..b3fb1d13c70 100644 --- a/lib/Manager.php +++ b/lib/Manager.php @@ -635,7 +635,6 @@ public function getRoomForUserByToken(string $token, ?string $userId, ?string $s } // never joined before but found in listing - $listable = (int)$row['listable']; if ($this->isRoomListableByUser($room, $userId)) { return $room; } @@ -732,6 +731,61 @@ public function getRoomByActor(string $token, string $actorType, string $actorId return $room; } + /** + * @param string $token + * @param string $actorType + * @param string $actorId + * @param string $remoteAccess + * @param ?string $sessionId + * @return Room + * @throws RoomNotFoundException + */ + public function getRoomByRemoteAccess(string $token, string $actorType, string $actorId, string $remoteAccess, ?string $sessionId = null): Room { + $query = $this->db->getQueryBuilder(); + $helper = new SelectHelper(); + $helper->selectRoomsTable($query); + $helper->selectAttendeesTable($query); + $query->from('talk_rooms', 'r') + ->leftJoin('r', 'talk_attendees', 'a', $query->expr()->andX( + $query->expr()->eq('a.actor_type', $query->createNamedParameter($actorType)), + $query->expr()->eq('a.actor_id', $query->createNamedParameter($actorId)), + $query->expr()->eq('a.access_token', $query->createNamedParameter($remoteAccess)), + $query->expr()->eq('a.room_id', 'r.id') + )) + ->where($query->expr()->eq('r.token', $query->createNamedParameter($token))); + + if ($sessionId !== null) { + $helper->selectSessionsTable($query); + $query->leftJoin('a', 'talk_sessions', 's', $query->expr()->andX( + $query->expr()->eq('s.session_id', $query->createNamedParameter($sessionId)), + $query->expr()->eq('a.id', 's.attendee_id') + )); + } + + $result = $query->executeQuery(); + $row = $result->fetch(); + $result->closeCursor(); + + if ($row === false) { + throw new RoomNotFoundException(); + } + + if ($row['token'] === null) { + // FIXME Temporary solution for the Talk6 release + throw new RoomNotFoundException(); + } + + $room = $this->createRoomObject($row); + if (isset($row['actor_id'])) { + $participant = $this->createParticipantObject($room, $row); + $this->participantService->cacheParticipant($room, $participant); + } else { + throw new RoomNotFoundException(); + } + + return $room; + } + /** * @param string $token * @param string|null $preloadUserId Load this participant's information if possible diff --git a/lib/Middleware/InjectionMiddleware.php b/lib/Middleware/InjectionMiddleware.php index 50ffda02218..0c0a939038d 100644 --- a/lib/Middleware/InjectionMiddleware.php +++ b/lib/Middleware/InjectionMiddleware.php @@ -54,16 +54,20 @@ use OCP\AppFramework\Middleware; use OCP\AppFramework\OCS\OCSException; use OCP\AppFramework\OCSController; +use OCP\Federation\ICloudIdManager; use OCP\IRequest; use OCP\Security\Bruteforce\IThrottler; class InjectionMiddleware extends Middleware { + protected bool $isTalkFederation = false; + protected ?string $federationCloudId = null; public function __construct( protected IRequest $request, protected ParticipantService $participantService, protected TalkSession $talkSession, protected Manager $manager, + protected ICloudIdManager $cloudIdManager, protected IThrottler $throttler, protected ?string $userId, ) { @@ -84,6 +88,11 @@ public function beforeController(Controller $controller, string $methodName): vo return; } + $this->isTalkFederation = (bool) $this->request->getHeader('X-Nextcloud-Federation'); + if ($this->isTalkFederation) { + $controller->setRemoteAccess($this->getRemoteAccessActorId(), $this->getRemoteAccessToken()); + } + $reflectionMethod = new \ReflectionMethod($controller, $methodName); $apiVersion = $this->request->getParam('apiVersion'); @@ -171,7 +180,15 @@ protected function getLoggedInOrGuest(AEnvironmentAwareController $controller, b if (!$room instanceof Room) { $token = $this->request->getParam('token'); $sessionId = $this->talkSession->getSessionForRoom($token); - $room = $this->manager->getRoomForUserByToken($token, $this->userId, $sessionId); + if (!$this->isTalkFederation) { + $room = $this->manager->getRoomForUserByToken($token, $this->userId, $sessionId); + } else { + $room = $this->manager->getRoomByRemoteAccess($token, Attendee::ACTOR_FEDERATED_USERS, $this->getRemoteAccessActorId(), $this->getRemoteAccessToken()); + + // Get and set the participant already so we don't retry public access + $participant = $this->participantService->getParticipantByActor($room, Attendee::ACTOR_FEDERATED_USERS, $this->getRemoteAccessActorId()); + $controller->setParticipant($participant); + } $controller->setRoom($room); } @@ -201,6 +218,27 @@ protected function getLoggedInOrGuest(AEnvironmentAwareController $controller, b } } + protected function getRemoteAccessActorId(): string { + if ($this->federationCloudId !== null) { + return $this->federationCloudId; + } + $authUser = $this->request->server['PHP_AUTH_USER'] ?? ''; + $authUser = urldecode($authUser); + + try { + $cloudId = $this->cloudIdManager->resolveCloudId($authUser); + $this->federationCloudId = $cloudId->getId(); + } catch (\InvalidArgumentException) { + $this->federationCloudId = ''; + } + + return $this->federationCloudId; + } + + protected function getRemoteAccessToken(): string { + return $this->request->server['PHP_AUTH_PW'] ?? ''; + } + /** * @param AEnvironmentAwareController $controller * @throws ReadOnlyException diff --git a/lib/Room.php b/lib/Room.php index 7f25a015513..1331cfe3470 100644 --- a/lib/Room.php +++ b/lib/Room.php @@ -467,6 +467,7 @@ public function isFederatedRemoteRoom(): bool { } public function setParticipant(?string $userId, Participant $participant): void { + // FIXME Also used with cloudId, need actorType checking? $this->currentUser = $userId; $this->participant = $participant; } diff --git a/lib/Service/ParticipantService.php b/lib/Service/ParticipantService.php index de482b49433..61a42d0bcbe 100644 --- a/lib/Service/ParticipantService.php +++ b/lib/Service/ParticipantService.php @@ -31,9 +31,11 @@ use OCA\Talk\Events\AddParticipantsEvent; use OCA\Talk\Events\AttendeesAddedEvent; use OCA\Talk\Events\AttendeesRemovedEvent; +use OCA\Talk\Events\BeforeFederatedUserJoinedRoomEvent; use OCA\Talk\Events\ChatEvent; use OCA\Talk\Events\DuplicatedParticipantEvent; use OCA\Talk\Events\EndCallForEveryoneEvent; +use OCA\Talk\Events\FederatedUserJoinedRoomEvent; use OCA\Talk\Events\JoinRoomGuestEvent; use OCA\Talk\Events\JoinRoomUserEvent; use OCA\Talk\Events\ModifyEveryoneEvent; @@ -78,7 +80,9 @@ class ParticipantService { - protected array $userCache; + /** @var array>> */ + protected array $actorCache; + /** @var array> */ protected array $sessionCache; public function __construct( @@ -338,6 +342,33 @@ public function joinRoom(RoomService $roomService, Room $room, IUser $user, stri return new Participant($room, $attendee, $session); } + /** + * @throws UnauthorizedException + */ + public function joinRoomAsFederatedUser(Room $room, string $actorType, string $actorId): Participant { + $event = new BeforeFederatedUserJoinedRoomEvent($room, $actorId); + $this->dispatcher->dispatchTyped($event); + + if ($event->isJoinCanceled()) { + throw new UnauthorizedException('Participant is not allowed to join'); + } + + try { + $participant = $this->getParticipantByActor($room, $actorType, $actorId); + $attendee = $participant->getAttendee(); + } catch (ParticipantNotFoundException $e) { + // shouldn't happen unless some code called joinRoom without previous checks + throw new UnauthorizedException('Participant is not allowed to join'); + } + + $session = $this->sessionService->createSessionForAttendee($attendee); + + $event = new FederatedUserJoinedRoomEvent($room, $actorId); + $this->dispatcher->dispatchTyped($event); + + return new Participant($room, $attendee, $session); + } + /** * @param RoomService $roomService * @param Room $room @@ -1602,12 +1633,10 @@ public function hasActiveSessions(Room $room): bool { public function cacheParticipant(Room $room, Participant $participant): void { $attendee = $participant->getAttendee(); - if ($attendee->getActorType() !== Attendee::ACTOR_USERS) { - return; - } - $this->userCache[$room->getId()] ??= []; - $this->userCache[$room->getId()][$attendee->getActorId()] = $participant; + $this->actorCache[$room->getId()] ??= []; + $this->actorCache[$room->getId()][$attendee->getActorType()] ??= []; + $this->actorCache[$room->getId()][$attendee->getActorType()][$attendee->getActorId()] = $participant; if ($participant->getSession()) { $participantSessionId = $participant->getSession()->getSessionId(); $this->sessionCache[$room->getId()] ??= []; @@ -1668,8 +1697,8 @@ public function getParticipant(Room $room, ?string $userId, $sessionId = null): throw new ParticipantNotFoundException('Not a user'); } - if (isset($this->userCache[$room->getId()][$userId])) { - $participant = $this->userCache[$room->getId()][$userId]; + if (isset($this->actorCache[$room->getId()][Attendee::ACTOR_USERS][$userId])) { + $participant = $this->actorCache[$room->getId()][Attendee::ACTOR_USERS][$userId]; if (!$sessionId || ($participant->getSession() instanceof Session && $participant->getSession()->getSessionId() === $sessionId)) { @@ -1701,8 +1730,9 @@ public function getParticipant(Room $room, ?string $userId, $sessionId = null): $participant = $this->getParticipantFromQuery($query, $room); - $this->userCache[$room->getId()] ??= []; - $this->userCache[$room->getId()][$userId] = $participant; + $this->actorCache[$room->getId()] ??= []; + $this->actorCache[$room->getId()][Attendee::ACTOR_USERS] ??= []; + $this->actorCache[$room->getId()][Attendee::ACTOR_USERS][$userId] = $participant; if ($participant->getSession()) { $participantSessionId = $participant->getSession()->getSessionId(); $this->sessionCache[$room->getId()] ??= []; @@ -1780,6 +1810,10 @@ public function getParticipantByAttendeeId(Room $room, int $attendeeId): Partici * @throws ParticipantNotFoundException When the pin is not valid (has no participant assigned) */ public function getParticipantByActor(Room $room, string $actorType, string $actorId): Participant { + if (isset($this->actorCache[$room->getId()][$actorType][$actorId])) { + return $this->actorCache[$room->getId()][$actorType][$actorId]; + } + if ($actorType === Attendee::ACTOR_USERS) { return $this->getParticipant($room, $actorId, false); } @@ -1793,6 +1827,8 @@ public function getParticipantByActor(Room $room, string $actorType, string $act ->andWhere($query->expr()->eq('a.room_id', $query->createNamedParameter($room->getId()))) ->setMaxResults(1); - return $this->getParticipantFromQuery($query, $room); + $participant = $this->getParticipantFromQuery($query, $room); + $this->cacheParticipant($room, $participant); + return $participant; } } diff --git a/lib/Service/RoomFormatter.php b/lib/Service/RoomFormatter.php index eac0b99bb26..0e5ed382e06 100644 --- a/lib/Service/RoomFormatter.php +++ b/lib/Service/RoomFormatter.php @@ -327,6 +327,12 @@ public function formatRoomV4( $roomData['lastReadMessage'] = $attendee->getLastReadMessage(); } + if ($room->getRemoteServer() && $room->getRemoteToken()) { + $roomData['remoteServer'] = $room->getRemoteServer(); + $roomData['remoteToken'] = $room->getRemoteToken(); + $roomData['remoteAccessToken'] = $attendee->getAccessToken(); + } + // FIXME This should not be done, but currently all the clients use it to get the avatar of the user … if ($room->getType() === Room::TYPE_ONE_TO_ONE) { $participants = json_decode($room->getName(), true); diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php index 91a1c43c170..b376f261686 100644 --- a/tests/integration/features/bootstrap/FeatureContext.php +++ b/tests/integration/features/bootstrap/FeatureContext.php @@ -57,6 +57,8 @@ class FeatureContext implements Context, SnippetAcceptingContext { protected static array $remoteToInviteId; /** @var array */ protected static array $inviteIdToRemote; + /** @var array */ + protected static array $remoteAuth; /** @var array */ protected static array $questionToPollId; /** @var array[] */ @@ -367,6 +369,9 @@ private function assertRooms($rooms, TableNode $formData, bool $shouldOrder = fa } Assert::assertEquals($expected, array_map(function ($room, $expectedRoom) { + if (isset($room['remoteAccessToken'])) { + self::$remoteAuth[self::translateRemoteServer($room['remoteServer']) . '#' . self::$identifierToToken[$room['name']]] = $room['remoteAccessToken']; + } if (!isset(self::$identifierToToken[$room['name']])) { self::$identifierToToken[$room['name']] = $room['token']; } @@ -1044,7 +1049,7 @@ public function userCreatesThePasswordRequestRoomForLastShare(string $user, int * @param TableNode|null $formData */ public function userJoinsRoom(string $user, string $identifier, int $statusCode, string $apiVersion, TableNode $formData = null): void { - $this->setCurrentUser($user); + $this->setCurrentUser($user, $identifier); $this->sendRequest( 'POST', '/apps/spreed/api/' . $apiVersion . '/room/' . self::$identifierToToken[$identifier] . '/participants/active', $formData @@ -1301,7 +1306,7 @@ public function userDeletesRoom(string $user, string $identifier, int $statusCod * @param string $apiVersion */ public function userGetsRoom(string $user, string $identifier, int $statusCode, string $apiVersion = 'v4', TableNode $formData = null): void { - $this->setCurrentUser($user); + $this->setCurrentUser($user, $identifier); $this->sendRequest('GET', '/apps/spreed/api/' . $apiVersion . '/room/' . self::$identifierToToken[$identifier]); $this->assertStatusCode($this->response, $statusCode); @@ -1777,7 +1782,7 @@ public function userSendsMessageToRoom(string $user, string $sendingMode, string $body = new TableNode([['message', $message]]); } - $this->setCurrentUser($user); + $this->setCurrentUser($user, $identifier); $this->sendRequest( 'POST', '/apps/spreed/api/' . $apiVersion . '/chat/' . self::$identifierToToken[$identifier], $body @@ -2448,7 +2453,7 @@ protected function compareDataResponse(TableNode $formData = null) { $data = [ 'room' => self::$tokenToIdentifier[$message['token']], 'actorType' => $message['actorType'], - 'actorId' => ($message['actorType'] === 'guests')? self::$sessionIdToUser[$message['actorId']]: $message['actorId'], + 'actorId' => $message['actorType'] === 'guests' ? self::$sessionIdToUser[$message['actorId']] : $message['actorId'], 'actorDisplayName' => $message['actorDisplayName'], // TODO test timestamp; it may require using Runkit, php-timecop // or something like that. @@ -3095,8 +3100,22 @@ public function resetGuestsAppState() { /** * @Given /^as user "([^"]*)"$/ */ - public function setCurrentUser(?string $user): ?string { + public function setCurrentUser(?string $user, ?string $identifier = null): ?string { $oldUser = $this->currentUser; + + if ($identifier && str_starts_with($user, 'federation/')) { + $user = substr($user, 11); + + $authArrayKey = 'LOCAL#' . self::$identifierToToken[$identifier]; + if (!isset(self::$remoteAuth[$authArrayKey])) { + throw new \Exception( + 'No remote auth available for: ' . 'LOCAL#' . self::$identifierToToken[$identifier] + . '. Did you pull rooms for the recipient? (user: ' . $user . ')' + ); + } + $user = 'federation#' . urlencode($user . '@' . 'http://localhost:8180') . '#' . self::$remoteAuth[$authArrayKey]; + } + $this->currentUser = $user; return $oldUser; } @@ -3977,6 +3996,11 @@ public function sendRequestFullUrl($verb, $fullUrl, $body = null, array $headers $options = array_merge($options, ['cookies' => $this->getUserCookieJar($this->currentUser)]); if ($this->currentUser === 'admin') { $options['auth'] = ['admin', 'admin']; + } elseif ($this->currentUser !== null && str_starts_with($this->currentUser, 'federation')) { + $auth = explode('#', $this->currentUser); + array_shift($auth); + $options['auth'] = $auth; + $headers['X-Nextcloud-Federation'] = 1; } elseif ($this->currentUser !== null && !str_starts_with($this->currentUser, 'guest')) { $options['auth'] = [$this->currentUser, self::TEST_PASSWORD]; } diff --git a/tests/integration/features/federation/invite.feature b/tests/integration/features/federation/invite.feature index 1ea48f73993..1bff38b3d69 100644 --- a/tests/integration/features/federation/invite.feature +++ b/tests/integration/features/federation/invite.feature @@ -78,3 +78,25 @@ Feature: federation/invite | room | federated_users | participant2@http://localhost:8180 | federated_user_removed | {federated_user} declined the invitation | {"actor":{"type":"user","id":"participant2","name":"participant2@localhost:8180","server":"http:\/\/localhost:8180"},"federated_user":{"type":"user","id":"participant2","name":"participant2@localhost:8180","server":"http:\/\/localhost:8180"}} | | room | users | participant1 | federated_user_added | You invited {user} | {"actor":{"type":"user","id":"participant1","name":"participant1-displayname"},"federated_user":{"type":"user","id":"participant2","name":"participant2@localhost:8180","server":"http:\/\/localhost:8180"}} | | room | users | participant1 | conversation_created | You created the conversation | {"actor":{"type":"user","id":"participant1","name":"participant1-displayname"}} | + + Scenario: Authenticate as a federation user + Given the following "spreed" app config is set + | federation_enabled | yes | + Given user "participant1" creates room "room" (v4) + | roomType | 2 | + | roomName | room | + And user "participant1" adds remote "participant2" to room "room" with 200 (v4) + And user "participant2" has the following invitations (v1) + | remote_server | remote_token | + | LOCAL | room | + And user "participant2" accepts invite to room "room" of server "LOCAL" (v1) + And user "participant2" has the following invitations (v1) + Then user "participant2" is participant of the following rooms (v4) + | id | type | + | room | 2 | + Then user "federation/participant2" gets room "room" with 200 (v4) + Then user "federation/participant2" joins room "room" with 200 (v4) + And user "federation/participant2" sends message "Message 1" to room "room" with 201 + Then user "participant1" sees the following messages in room "room" with 200 + | room | actorType | actorId | actorDisplayName | message | messageParameters | parentMessage | + | room |federated_users | participant2@http://localhost:8180 | participant2@http://localhost:8180 | Message 1 | [] | | diff --git a/tests/psalm-baseline.xml b/tests/psalm-baseline.xml index 930eb79e1c0..05576b9973c 100644 --- a/tests/psalm-baseline.xml +++ b/tests/psalm-baseline.xml @@ -1,5 +1,5 @@ - + BeforeTemplateRenderedEvent @@ -65,6 +65,12 @@ private + + + request->server]]> + request->server]]> + +