Browse Source

feat: Join and leave federated participants using their federated session

Unfortunately this prevents to join a room as a local federated user, as
in that case the session will be already in use.

On the other hand, this avoids the need to convert sessions between
host and federated servers, and with that also avoids having to keep a
map between sessions.

Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
pull/12604/head
Daniel Calviño Sánchez 1 year ago
committed by Joas Schilling
parent
commit
255794c3dd
No known key found for this signature in database GPG Key ID: 74434EFE0D2E2205
  1. 2
      appinfo/routes/routesRoomController.php
  2. 114
      lib/Controller/RoomController.php
  3. 45
      lib/Federation/Proxy/TalkV1/Controller/RoomController.php
  4. 4
      lib/Service/ParticipantService.php
  5. 23
      tests/integration/features/bootstrap/FeatureContext.php
  6. 110
      tests/integration/features/federation/join-leave.feature

2
appinfo/routes/routesRoomController.php

@ -71,6 +71,8 @@ return [
['name' => 'Room#resendInvitations', 'url' => '/api/{apiVersion}/room/{token}/participants/resend-invitations', 'verb' => 'POST', 'requirements' => $requirementsWithToken],
/** @see \OCA\Talk\Controller\RoomController::leaveRoom() */
['name' => 'Room#leaveRoom', 'url' => '/api/{apiVersion}/room/{token}/participants/active', 'verb' => 'DELETE', 'requirements' => $requirementsWithToken],
/** @see \OCA\Talk\Controller\RoomController::leaveFederatedRoom() */
['name' => 'Room#leaveFederatedRoom', 'url' => '/api/{apiVersion}/room/{token}/federation/active', 'verb' => 'DELETE', 'requirements' => $requirementsWithToken],
/** @see \OCA\Talk\Controller\RoomController::setSessionState() */
['name' => 'Room#setSessionState', 'url' => '/api/{apiVersion}/room/{token}/participants/state', 'verb' => 'PUT', 'requirements' => $requirementsWithToken],
/** @see \OCA\Talk\Controller\RoomController::promoteModerator() */

114
lib/Controller/RoomController.php

@ -1564,22 +1564,6 @@ class RoomController extends AEnvironmentAwareController {
$headers = [];
if ($room->isFederatedConversation()) {
$participant = $this->participantService->getParticipant($room, $this->userId);
/** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\RoomController $proxy */
$proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\RoomController::class);
$response = $proxy->joinFederatedRoom($room, $participant);
if ($response->getStatus() === Http::STATUS_NOT_FOUND) {
$this->participantService->removeAttendee($room, $participant, AAttendeeRemovedEvent::REASON_REMOVED);
return new DataResponse([], Http::STATUS_NOT_FOUND);
}
$proxyHeaders = $response->getHeaders();
if (isset($proxyHeaders['X-Nextcloud-Talk-Proxy-Hash'])) {
$headers['X-Nextcloud-Talk-Proxy-Hash'] = $proxyHeaders['X-Nextcloud-Talk-Proxy-Hash'];
}
// Skip password checking
$result = [
'result' => true,
@ -1618,23 +1602,49 @@ class RoomController extends AEnvironmentAwareController {
$this->sessionService->updateLastPing($session, $this->timeFactory->getTime());
}
if ($room->isFederatedConversation()) {
/** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\RoomController $proxy */
$proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\RoomController::class);
try {
$response = $proxy->joinFederatedRoom($room, $participant);
} catch (CannotReachRemoteException $e) {
$this->participantService->leaveRoomAsSession($room, $participant);
throw $e;
}
if ($response->getStatus() === Http::STATUS_NOT_FOUND) {
$this->participantService->removeAttendee($room, $participant, AAttendeeRemovedEvent::REASON_REMOVED);
return new DataResponse([], Http::STATUS_NOT_FOUND);
}
$proxyHeaders = $response->getHeaders();
if (isset($proxyHeaders['X-Nextcloud-Talk-Proxy-Hash'])) {
$headers['X-Nextcloud-Talk-Proxy-Hash'] = $proxyHeaders['X-Nextcloud-Talk-Proxy-Hash'];
}
}
return new DataResponse($this->formatRoom($room, $participant), Http::STATUS_OK, $headers);
}
/**
* Fake join a room on the host server to verify the federated user is still part of it
* Join room on the host server using the session id of the federated user.
*
* The session id can be null only for requests from Talk < 20.
*
* @param string $token Token of the room
* @param string $sessionId Federated session id to join with
* @return DataResponse<Http::STATUS_OK, array<empty>, array{X-Nextcloud-Talk-Hash: string}>|DataResponse<Http::STATUS_NOT_FOUND, null, array{}>
*
* 200: Federated user is still part of the room
* 200: Federated user joined the room
* 404: Room not found
*/
#[OpenAPI(scope: OpenAPI::SCOPE_FEDERATION)]
#[PublicPage]
#[BruteForceProtection(action: 'talkRoomToken')]
#[BruteForceProtection(action: 'talkFederationAccess')]
public function joinFederatedRoom(string $token): DataResponse {
public function joinFederatedRoom(string $token, ?string $sessionId): DataResponse {
if (!$this->federationAuthenticator->isFederationRequest()) {
$response = new DataResponse(null, Http::STATUS_NOT_FOUND);
$response->throttle(['token' => $token, 'action' => 'talkRoomToken']);
@ -1643,9 +1653,9 @@ class RoomController extends AEnvironmentAwareController {
try {
try {
$this->federationAuthenticator->getRoom();
$room = $this->federationAuthenticator->getRoom();
} catch (RoomNotFoundException) {
$this->manager->getRoomByRemoteAccess(
$room = $this->manager->getRoomByRemoteAccess(
$token,
Attendee::ACTOR_FEDERATED_USERS,
$this->federationAuthenticator->getCloudId(),
@ -1653,12 +1663,16 @@ class RoomController extends AEnvironmentAwareController {
);
}
if ($sessionId != null) {
$participant = $this->participantService->joinRoomAsFederatedUser($room, Attendee::ACTOR_FEDERATED_USERS, $this->federationAuthenticator->getCloudId(), $sessionId);
}
// Let the clients know if they need to reload capabilities
$capabilities = $this->capabilities->getCapabilities();
return new DataResponse([], Http::STATUS_OK, [
'X-Nextcloud-Talk-Hash' => sha1(json_encode($capabilities)),
]);
} catch (RoomNotFoundException) {
} catch (RoomNotFoundException|UnauthorizedException) {
$response = new DataResponse(null, Http::STATUS_NOT_FOUND);
$response->throttle(['token' => $token, 'action' => 'talkFederationAccess']);
return $response;
@ -1885,6 +1899,62 @@ class RoomController extends AEnvironmentAwareController {
try {
$room = $this->manager->getRoomForUserByToken($token, $this->userId, $sessionId);
$participant = $this->participantService->getParticipantBySession($room, $sessionId);
if ($room->isFederatedConversation()) {
/** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\RoomController $proxy */
$proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\RoomController::class);
$response = $proxy->leaveFederatedRoom($room, $participant);
}
$this->participantService->leaveRoomAsSession($room, $participant);
} catch (RoomNotFoundException|ParticipantNotFoundException) {
}
return new DataResponse();
}
/**
* Leave room on the host server using the session id of the federated user.
*
* @param string $token Token of the room
* @param string $sessionId Federated session id to leave with
* @return DataResponse<Http::STATUS_OK, array<empty>, array{}>|DataResponse<Http::STATUS_NOT_FOUND, null, array{}>
*
* 200: Successfully left the room
* 404: Room not found (non-federation request)
*/
#[OpenAPI(scope: OpenAPI::SCOPE_FEDERATION)]
#[PublicPage]
#[BruteForceProtection(action: 'talkRoomToken')]
public function leaveFederatedRoom(string $token, string $sessionId): DataResponse {
if (!$this->federationAuthenticator->isFederationRequest()) {
$response = new DataResponse(null, Http::STATUS_NOT_FOUND);
$response->throttle(['token' => $token, 'action' => 'talkRoomToken']);
return $response;
}
try {
try {
$room = $this->federationAuthenticator->getRoom();
} catch (RoomNotFoundException) {
$room = $this->manager->getRoomByRemoteAccess(
$token,
Attendee::ACTOR_FEDERATED_USERS,
$this->federationAuthenticator->getCloudId(),
$this->federationAuthenticator->getAccessToken(),
);
}
try {
$participant = $this->federationAuthenticator->getParticipant();
} catch (ParticipantNotFoundException) {
$participant = $this->participantService->getParticipantBySession(
$room,
$sessionId,
);
$this->federationAuthenticator->authenticated($room, $participant);
}
$this->participantService->leaveRoomAsSession($room, $participant);
} catch (RoomNotFoundException|ParticipantNotFoundException) {
}

45
lib/Federation/Proxy/TalkV1/Controller/RoomController.php

@ -11,6 +11,7 @@ namespace OCA\Talk\Federation\Proxy\TalkV1\Controller;
use OCA\Talk\Exceptions\CannotReachRemoteException;
use OCA\Talk\Federation\Proxy\TalkV1\ProxyRequest;
use OCA\Talk\Federation\Proxy\TalkV1\UserConverter;
use OCA\Talk\Model\Session;
use OCA\Talk\Participant;
use OCA\Talk\ResponseDefinitions;
use OCA\Talk\Room;
@ -63,17 +64,25 @@ class RoomController {
/**
* @see \OCA\Talk\Controller\RoomController::joinFederatedRoom()
*
* @param Room $room the federated room to join
* @param Participant $participant the federated user to will join the room;
* the participant must have a session
* @return DataResponse<Http::STATUS_OK|Http::STATUS_NOT_FOUND, array<empty>, array{X-Nextcloud-Talk-Proxy-Hash: string}>
* @throws CannotReachRemoteException
*
* 200: Federated user is still part of the room
* 200: Federated user joined the room
* 404: Room not found
*/
public function joinFederatedRoom(Room $room, Participant $participant): DataResponse {
$options = [
'sessionId' => $participant->getSession()->getSessionId(),
];
$proxy = $this->proxy->post(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v4/room/' . $room->getRemoteToken() . '/federation/active',
$options,
);
$statusCode = $proxy->getStatusCode();
@ -87,6 +96,40 @@ class RoomController {
return new DataResponse([], $statusCode, $headers);
}
/**
* @see \OCA\Talk\Controller\RoomController::leaveFederatedRoom()
*
* @param Room $room the federated room to leave
* @param Participant $participant the federated user that will leave the
* room; the participant must have a session
* @return DataResponse<Http::STATUS_OK, array<empty>, array{}>
* @throws CannotReachRemoteException
*
* 200: Federated user left the room
*/
public function leaveFederatedRoom(Room $room, Participant $participant): DataResponse {
$options = [
'sessionId' => $participant->getSession()->getSessionId(),
];
$proxy = $this->proxy->delete(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v4/room/' . $room->getRemoteToken() . '/federation/active',
$options,
);
// STATUS_NOT_FOUND is not taken into account, as it should happen only
// for non-federation requests.
$statusCode = $proxy->getStatusCode();
if (!in_array($statusCode, [Http::STATUS_OK], true)) {
$this->proxy->logUnexpectedStatusCode(__METHOD__, $proxy->getStatusCode());
throw new CannotReachRemoteException();
}
return new DataResponse([], $statusCode);
}
/**
* @see \OCA\Talk\Controller\RoomController::getCapabilities()
*

4
lib/Service/ParticipantService.php

@ -363,7 +363,7 @@ class ParticipantService {
/**
* @throws UnauthorizedException
*/
public function joinRoomAsFederatedUser(Room $room, string $actorType, string $actorId): Participant {
public function joinRoomAsFederatedUser(Room $room, string $actorType, string $actorId, string $sessionId): Participant {
$event = new BeforeFederatedUserJoinedRoomEvent($room, $actorId);
$this->dispatcher->dispatchTyped($event);
@ -379,7 +379,7 @@ class ParticipantService {
throw new UnauthorizedException('Participant is not allowed to join');
}
$session = $this->sessionService->createSessionForAttendee($attendee);
$session = $this->sessionService->createSessionForAttendee($attendee, $sessionId);
$event = new FederatedUserJoinedRoomEvent($room, $actorId);
$this->dispatcher->dispatchTyped($event);

23
tests/integration/features/bootstrap/FeatureContext.php

@ -820,6 +820,19 @@ class FeatureContext implements Context, SnippetAcceptingContext {
if (isset($expectedKeys['callId'])) {
$data['callId'] = (string) $attendee['callId'];
}
if (isset($expectedKeys['sessionIds'])) {
$sessionIds = '[';
foreach ($attendee['sessionIds'] as $sessionId) {
if (str_contains($sessionId, '#')) {
$sessionIds .= 'SESSION' . substr($sessionId, strpos($sessionId, '#')) . ',';
} else {
$sessionIds .= 'SESSION,';
}
}
$sessionIds .= ']';
$data['sessionIds'] = $sessionIds;
}
if (!isset(self::$userToAttendeeId[$identifier][$attendee['actorType']])) {
self::$userToAttendeeId[$identifier][$attendee['actorType']] = [];
@ -853,6 +866,16 @@ class FeatureContext implements Context, SnippetAcceptingContext {
$attendee['actorId'] .= '@' . rtrim($this->localRemoteServerUrl, '/');
}
if (isset($attendee['sessionIds']) && str_contains($attendee['sessionIds'], '@{$LOCAL_URL}')) {
$attendee['sessionIds'] = str_replace('{$LOCAL_URL}', rtrim($this->localServerUrl, '/'), $attendee['sessionIds']);
}
if (isset($attendee['sessionIds']) && str_contains($attendee['sessionIds'], '@{$LOCAL_REMOTE_URL}')) {
$attendee['sessionIds'] = str_replace('{$LOCAL_REMOTE_URL}', rtrim($this->localRemoteServerUrl, '/'), $attendee['sessionIds']);
}
if (isset($attendee['sessionIds']) && str_contains($attendee['sessionIds'], '@{$REMOTE_URL}')) {
$attendee['sessionIds'] = str_replace('{$REMOTE_URL}', rtrim($this->remoteServerUrl, '/'), $attendee['sessionIds']);
}
if (isset($attendee['actorId'], $attendee['actorType'], $attendee['phoneNumber'])
&& $attendee['actorType'] === 'phones'
&& str_starts_with($attendee['actorId'], 'PHONE(')) {

110
tests/integration/features/federation/join-leave.feature

@ -0,0 +1,110 @@
Feature: federation/join-leave
Background:
Given using server "REMOTE"
And user "participant2" exists
And the following "spreed" app config is set
| federation_enabled | yes |
And using server "LOCAL"
And user "participant1" exists
And the following "spreed" app config is set
| federation_enabled | yes |
Scenario: join a group room
Given user "participant1" creates room "room" (v4)
| roomType | 2 |
| roomName | room |
And user "participant1" adds federated_user "participant2@REMOTE" to room "room" with 200 (v4)
And using server "REMOTE"
And user "participant2" has the following invitations (v1)
| remoteServerUrl | remoteToken | state | inviterCloudId | inviterDisplayName |
| LOCAL | room | 0 | participant1@http://localhost:8080 | participant1-displayname |
And user "participant2" accepts invite to room "room" of server "LOCAL" with 200 (v1)
| id | name | type | remoteServer | remoteToken |
| LOCAL::room | room | 2 | LOCAL | room |
When using server "LOCAL"
And user "participant1" joins room "room" with 200 (v4)
And using server "REMOTE"
And user "participant2" joins room "LOCAL::room" with 200 (v4)
Then using server "LOCAL"
And user "participant1" is participant of room "room" (v4)
And user "participant1" sees the following attendees in room "room" with 200 (v4)
| actorType | actorId | participantType | sessionIds |
| users | participant1 | 1 | [SESSION,] |
| federated_users | participant2@{$REMOTE_URL} | 3 | [SESSION#participant2@{$REMOTE_URL},] |
And using server "REMOTE"
And user "participant2" is participant of room "LOCAL::room" (v4)
And user "participant2" sees the following attendees in room "LOCAL::room" with 200 (v4)
| actorType | actorId | participantType | sessionIds |
| federated_users | participant1@{$LOCAL_URL} | 1 | [SESSION,] |
| users | participant2 | 3 | [SESSION#participant2@{$REMOTE_URL},] |
Scenario: join a group room again without leaving it first
Given user "participant1" creates room "room" (v4)
| roomType | 2 |
| roomName | room |
And user "participant1" adds federated_user "participant2@REMOTE" to room "room" with 200 (v4)
And using server "REMOTE"
And user "participant2" has the following invitations (v1)
| remoteServerUrl | remoteToken | state | inviterCloudId | inviterDisplayName |
| LOCAL | room | 0 | participant1@http://localhost:8080 | participant1-displayname |
And user "participant2" accepts invite to room "room" of server "LOCAL" with 200 (v1)
| id | name | type | remoteServer | remoteToken |
| LOCAL::room | room | 2 | LOCAL | room |
And using server "LOCAL"
And user "participant1" joins room "room" with 200 (v4)
And using server "REMOTE"
And user "participant2" joins room "LOCAL::room" with 200 (v4)
When user "participant2" joins room "LOCAL::room" with 200 (v4)
Then using server "LOCAL"
And user "participant1" is participant of room "room" (v4)
And user "participant1" sees the following attendees in room "room" with 200 (v4)
| actorType | actorId | participantType | sessionIds |
| users | participant1 | 1 | [SESSION,] |
| federated_users | participant2@{$REMOTE_URL} | 3 | [SESSION#participant2@{$REMOTE_URL},] |
And using server "REMOTE"
And user "participant2" is participant of room "LOCAL::room" (v4)
And user "participant2" sees the following attendees in room "LOCAL::room" with 200 (v4)
| actorType | actorId | participantType | sessionIds |
| federated_users | participant1@{$LOCAL_URL} | 1 | [SESSION,] |
| users | participant2 | 3 | [SESSION#participant2@{$REMOTE_URL},] |
Scenario: leave a group room
Given user "participant1" creates room "room" (v4)
| roomType | 2 |
| roomName | room |
And user "participant1" adds federated_user "participant2@REMOTE" to room "room" with 200 (v4)
And using server "REMOTE"
And user "participant2" has the following invitations (v1)
| remoteServerUrl | remoteToken | state | inviterCloudId | inviterDisplayName |
| LOCAL | room | 0 | participant1@http://localhost:8080 | participant1-displayname |
And user "participant2" accepts invite to room "room" of server "LOCAL" with 200 (v1)
| id | name | type | remoteServer | remoteToken |
| LOCAL::room | room | 2 | LOCAL | room |
And using server "LOCAL"
And user "participant1" joins room "room" with 200 (v4)
And using server "REMOTE"
And user "participant2" joins room "LOCAL::room" with 200 (v4)
And using server "LOCAL"
And user "participant1" sees the following attendees in room "room" with 200 (v4)
| actorType | actorId | participantType | sessionIds |
| users | participant1 | 1 | [SESSION,] |
| federated_users | participant2@{$REMOTE_URL} | 3 | [SESSION#participant2@{$REMOTE_URL},] |
And using server "REMOTE"
And user "participant2" sees the following attendees in room "LOCAL::room" with 200 (v4)
| actorType | actorId | participantType | sessionIds |
| federated_users | participant1@{$LOCAL_URL} | 1 | [SESSION,] |
| users | participant2 | 3 | [SESSION#participant2@{$REMOTE_URL},] |
When user "participant2" leaves room "LOCAL::room" with 200 (v4)
Then using server "LOCAL"
And user "participant1" is participant of room "room" (v4)
And user "participant1" sees the following attendees in room "room" with 200 (v4)
| actorType | actorId | participantType | sessionIds |
| users | participant1 | 1 | [SESSION,] |
| federated_users | participant2@{$REMOTE_URL} | 3 | [] |
And using server "REMOTE"
And user "participant2" is participant of room "LOCAL::room" (v4)
And user "participant2" sees the following attendees in room "LOCAL::room" with 200 (v4)
| actorType | actorId | participantType | sessionIds |
| federated_users | participant1@{$LOCAL_URL} | 1 | [SESSION,] |
| users | participant2 | 3 | [] |
Loading…
Cancel
Save