diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 6d6b295fe0f..21fa3b69705 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -72,6 +72,7 @@ use OCA\Talk\Federation\Proxy\TalkV1\Notifier\BeforeRoomDeletedListener as TalkV1BeforeRoomDeletedListener; use OCA\Talk\Federation\Proxy\TalkV1\Notifier\CancelRetryOCMListener as TalkV1CancelRetryOCMListener; use OCA\Talk\Federation\Proxy\TalkV1\Notifier\MessageSentListener as TalkV1MessageSentListener; +use OCA\Talk\Federation\Proxy\TalkV1\Notifier\ParticipantModifiedListener as TalkV1ParticipantModifiedListener; use OCA\Talk\Federation\Proxy\TalkV1\Notifier\RoomModifiedListener as TalkV1RoomModifiedListener; use OCA\Talk\Files\Listener as FilesListener; use OCA\Talk\Files\TemplateLoader as FilesTemplateLoader; @@ -268,6 +269,7 @@ public function register(IRegistrationContext $context): void { // Federation listeners $context->registerEventListener(BeforeRoomDeletedEvent::class, TalkV1BeforeRoomDeletedListener::class); + $context->registerEventListener(ParticipantModifiedEvent::class, TalkV1ParticipantModifiedListener::class); $context->registerEventListener(CallEndedEvent::class, TalkV1RoomModifiedListener::class); $context->registerEventListener(CallEndedForEveryoneEvent::class, TalkV1RoomModifiedListener::class); $context->registerEventListener(CallStartedEvent::class, TalkV1RoomModifiedListener::class); diff --git a/lib/Federation/BackendNotifier.php b/lib/Federation/BackendNotifier.php index 6aff638db4b..5900855b6de 100644 --- a/lib/Federation/BackendNotifier.php +++ b/lib/Federation/BackendNotifier.php @@ -69,6 +69,7 @@ public function sendRemoteShare( $roomName = $room->getName(); $roomType = $room->getType(); $roomToken = $room->getToken(); + $roomDefaultPermissions = $room->getDefaultPermissions(); try { $this->restrictionValidator->isAllowedToInvite($sharedBy, $invitedCloudId); @@ -101,6 +102,7 @@ public function sendRemoteShare( $protocol['invitedCloudId'] = $invitedCloudId->getId(); $protocol['roomName'] = $roomName; $protocol['roomType'] = $roomType; + $protocol['roomDefaultPermissions'] = $roomDefaultPermissions; $protocol['name'] = FederationManager::TALK_PROTOCOL_NAME; $share->setProtocol($protocol); @@ -251,6 +253,40 @@ public function sendRoomModifiedUpdate( return $this->sendUpdateToRemote($remote, $notification); } + /** + * Send information to remote participants that the participant meta info updated + * Sent from Host server to Remote participant server (only for the affected participant) + */ + public function sendParticipantModifiedUpdate( + string $remoteServer, + int $localAttendeeId, + #[SensitiveParameter] + string $accessToken, + string $localToken, + string $changedProperty, + string|int $newValue, + string|int|null $oldValue, + ): ?bool { + $remote = $this->prepareRemoteUrl($remoteServer); + + $notification = $this->cloudFederationFactory->getCloudFederationNotification(); + $notification->setMessage( + FederationManager::NOTIFICATION_PARTICIPANT_MODIFIED, + FederationManager::TALK_ROOM_RESOURCE, + (string) $localAttendeeId, + [ + 'remoteServerUrl' => $this->getServerRemoteUrl(), + 'sharedSecret' => $accessToken, + 'remoteToken' => $localToken, + 'changedProperty' => $changedProperty, + 'newValue' => $newValue, + 'oldValue' => $oldValue, + ], + ); + + return $this->sendUpdateToRemote($remote, $notification); + } + /** * Send information to remote participants that "active since" was updated * Sent from Host server to Remote participant server diff --git a/lib/Federation/CloudFederationProviderTalk.php b/lib/Federation/CloudFederationProviderTalk.php index 49d65b65823..5fd5b6b50c0 100644 --- a/lib/Federation/CloudFederationProviderTalk.php +++ b/lib/Federation/CloudFederationProviderTalk.php @@ -125,6 +125,7 @@ public function shareReceived(ICloudFederationShare $share): string { $remoteId = $share->getProviderId(); $roomToken = $share->getResourceName(); $roomName = $share->getProtocol()['roomName']; + $roomDefaultPermissions = $share->getProtocol()['roomDefaultPermissions'] ?? Attendee::PERMISSIONS_DEFAULT; if (isset($share->getProtocol()['invitedCloudId'])) { $localCloudId = $share->getProtocol()['invitedCloudId']; } else { @@ -173,7 +174,7 @@ public function shareReceived(ICloudFederationShare $share): string { throw new ProviderCouldNotAddShareException('User does not exist', '', Http::STATUS_BAD_REQUEST); } - $invite = $this->federationManager->addRemoteRoom($shareWithUser, (int) $remoteId, $roomType, $roomName, $roomToken, $remote, $shareSecret, $sharedByFederatedId, $sharedByDisplayName, $localCloudId); + $invite = $this->federationManager->addRemoteRoom($shareWithUser, (int) $remoteId, $roomType, $roomName, $roomDefaultPermissions, $roomToken, $remote, $shareSecret, $sharedByFederatedId, $sharedByDisplayName, $localCloudId); $this->notifyAboutNewShare($shareWithUser, (string) $invite->getId(), $sharedByFederatedId, $sharedByDisplayName, $roomName, $roomToken, $remote); return (string) $invite->getId(); @@ -197,6 +198,8 @@ public function notificationReceived($notificationType, $providerId, array $noti return $this->shareDeclined((int) $providerId, $notification); case FederationManager::NOTIFICATION_SHARE_UNSHARED: return $this->shareUnshared((int) $providerId, $notification); + case FederationManager::NOTIFICATION_PARTICIPANT_MODIFIED: + return $this->participantModified((int) $providerId, $notification); case FederationManager::NOTIFICATION_ROOM_MODIFIED: return $this->roomModified((int) $providerId, $notification); case FederationManager::NOTIFICATION_MESSAGE_POSTED: @@ -292,6 +295,42 @@ private function shareUnshared(int $remoteAttendeeId, array $notification): arra return []; } + /** + * @param int $remoteAttendeeId + * @param array{remoteServerUrl: string, sharedSecret: string, remoteToken: string, changedProperty: string, newValue: string|int, oldValue: string|int|null} $notification + * @return array + * @throws ActionNotSupportedException + * @throws AuthenticationFailedException + * @throws ShareNotFound + */ + private function participantModified(int $remoteAttendeeId, array $notification): array { + $invite = $this->getByRemoteAttendeeAndValidate($notification['remoteServerUrl'], $remoteAttendeeId, $notification['sharedSecret']); + try { + $room = $this->manager->getRoomById($invite->getLocalRoomId()); + } catch (RoomNotFoundException) { + throw new ShareNotFound(FederationManager::OCM_RESOURCE_NOT_FOUND); + } + + // Sanity check to make sure the room is a remote room + if (!$room->isFederatedConversation()) { + throw new ShareNotFound(FederationManager::OCM_RESOURCE_NOT_FOUND); + } + + try { + $participant = $this->participantService->getParticipant($room, $invite->getUserId()); + } catch (ParticipantNotFoundException $e) { + throw new ShareNotFound(FederationManager::OCM_RESOURCE_NOT_FOUND); + } + + if ($notification['changedProperty'] === AParticipantModifiedEvent::PROPERTY_PERMISSIONS) { + $this->participantService->updatePermissions($room, $participant, Attendee::PERMISSIONS_MODIFY_SET, $notification['newValue']); + } else { + $this->logger->debug('Update of participant property "' . $notification['changedProperty'] . '" is not handled and should not be send via federation'); + } + + return []; + } + /** * @param int $remoteAttendeeId * @param array{remoteServerUrl: string, sharedSecret: string, remoteToken: string, changedProperty: string, newValue: string|int|bool|null, oldValue: string|int|bool|null, callFlag?: int, dateTime?: string, timerReached?: bool, details?: array} $notification @@ -324,6 +363,8 @@ private function roomModified(int $remoteAttendeeId, array $notification): array $this->roomService->setAvatar($room, $notification['newValue']); } elseif ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_CALL_RECORDING) { $this->roomService->setCallRecording($room, $notification['newValue']); + } elseif ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_DEFAULT_PERMISSIONS) { + $this->roomService->setDefaultPermissions($room, $notification['newValue']); } elseif ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_DESCRIPTION) { $this->roomService->setDescription($room, $notification['newValue']); } elseif ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_IN_CALL) { diff --git a/lib/Federation/FederationManager.php b/lib/Federation/FederationManager.php index 9e022a9a66c..e350897498f 100644 --- a/lib/Federation/FederationManager.php +++ b/lib/Federation/FederationManager.php @@ -19,6 +19,7 @@ use OCA\Talk\Participant; use OCA\Talk\Room; use OCA\Talk\Service\ParticipantService; +use OCA\Talk\Service\RoomService; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Http; use OCP\Federation\Exceptions\ProviderCouldNotAddShareException; @@ -42,6 +43,7 @@ class FederationManager { public const NOTIFICATION_SHARE_ACCEPTED = 'SHARE_ACCEPTED'; public const NOTIFICATION_SHARE_DECLINED = 'SHARE_DECLINED'; public const NOTIFICATION_SHARE_UNSHARED = 'SHARE_UNSHARED'; + public const NOTIFICATION_PARTICIPANT_MODIFIED = 'PARTICIPANT_MODIFIED'; public const NOTIFICATION_ROOM_MODIFIED = 'ROOM_MODIFIED'; public const NOTIFICATION_MESSAGE_POSTED = 'MESSAGE_POSTED'; public const TOKEN_LENGTH = 64; @@ -49,6 +51,7 @@ class FederationManager { public function __construct( private Manager $manager, private ParticipantService $participantService, + private RoomService $roomService, private InvitationMapper $invitationMapper, private BackendNotifier $backendNotifier, private IManager $notificationManager, @@ -74,6 +77,7 @@ public function addRemoteRoom( int $remoteAttendeeId, int $roomType, string $roomName, + int $roomDefaultPermissions, string $remoteToken, string $remoteServerUrl, #[SensitiveParameter] @@ -90,6 +94,13 @@ public function addRemoteRoom( $room = $this->manager->createRemoteRoom($roomType, $roomName, $remoteToken, $remoteServerUrl); } + // Only update the room permissions if there are no participants in the + // remote room. Otherwise, the room permissions would be up to date + // already due to the notifications about room permission changes. + if (!$this->participantService->getNumberOfActors($room)) { + $this->roomService->setDefaultPermissions($room, $roomDefaultPermissions); + } + if ($couldHaveInviteWithOtherCasing) { try { $this->invitationMapper->getInvitationForUserByLocalRoom($room, $user->getUID(), true); diff --git a/lib/Federation/Proxy/TalkV1/Notifier/ParticipantModifiedListener.php b/lib/Federation/Proxy/TalkV1/Notifier/ParticipantModifiedListener.php new file mode 100644 index 00000000000..cacbccace12 --- /dev/null +++ b/lib/Federation/Proxy/TalkV1/Notifier/ParticipantModifiedListener.php @@ -0,0 +1,70 @@ + + */ +class ParticipantModifiedListener implements IEventListener { + public function __construct( + protected BackendNotifier $backendNotifier, + protected ParticipantService $participantService, + protected ICloudIdManager $cloudIdManager, + ) { + } + + public function handle(Event $event): void { + if (!$event instanceof ParticipantModifiedEvent) { + return; + } + + $participant = $event->getParticipant(); + if ($participant->getAttendee()->getActorType() !== Attendee::ACTOR_FEDERATED_USERS) { + return; + } + + if (!in_array($event->getProperty(), [ + AParticipantModifiedEvent::PROPERTY_PERMISSIONS, + ], true)) { + return; + } + + // For modifying participants we only notify the affected participant's server + $cloudId = $this->cloudIdManager->resolveCloudId($participant->getAttendee()->getActorId()); + $success = $this->notifyParticipantModified($cloudId, $participant, $event); + + if ($success === null) { + $this->participantService->removeAttendee($event->getRoom(), $participant, AAttendeeRemovedEvent::REASON_LEFT); + } + } + + private function notifyParticipantModified(ICloudId $cloudId, Participant $participant, AParticipantModifiedEvent $event): ?bool { + return $this->backendNotifier->sendParticipantModifiedUpdate( + $cloudId->getRemote(), + $participant->getAttendee()->getId(), + $participant->getAttendee()->getAccessToken(), + $event->getRoom()->getToken(), + $event->getProperty(), + $event->getNewValue(), + $event->getOldValue(), + ); + } +} diff --git a/lib/Federation/Proxy/TalkV1/Notifier/RoomModifiedListener.php b/lib/Federation/Proxy/TalkV1/Notifier/RoomModifiedListener.php index a81fedd34f9..a5cc88d7f5a 100644 --- a/lib/Federation/Proxy/TalkV1/Notifier/RoomModifiedListener.php +++ b/lib/Federation/Proxy/TalkV1/Notifier/RoomModifiedListener.php @@ -50,6 +50,7 @@ public function handle(Event $event): void { ARoomModifiedEvent::PROPERTY_ACTIVE_SINCE, ARoomModifiedEvent::PROPERTY_AVATAR, ARoomModifiedEvent::PROPERTY_CALL_RECORDING, + ARoomModifiedEvent::PROPERTY_DEFAULT_PERMISSIONS, ARoomModifiedEvent::PROPERTY_DESCRIPTION, ARoomModifiedEvent::PROPERTY_IN_CALL, ARoomModifiedEvent::PROPERTY_LOBBY, diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php index 517a4c3dbe7..d7721eb7a47 100644 --- a/tests/integration/features/bootstrap/FeatureContext.php +++ b/tests/integration/features/bootstrap/FeatureContext.php @@ -562,9 +562,6 @@ private function assertRooms(array $rooms, TableNode $formData, bool $shouldOrde if (isset($expectedRoom['permissions'])) { $data['permissions'] = $this->mapPermissionsAPIOutput($room['permissions']); } - if (isset($expectedRoom['permissions'])) { - $data['permissions'] = $this->mapPermissionsAPIOutput($room['permissions']); - } if (isset($expectedRoom['attendeePermissions'])) { $data['attendeePermissions'] = $this->mapPermissionsAPIOutput($room['attendeePermissions']); } @@ -2019,6 +2016,12 @@ public function userSetsPermissionsForInRoomTo(string $user, string $participant } elseif (strpos($participant, 'guest') === 0) { $sessionId = self::$userToSessionId[$participant]; $attendeeId = $this->getAttendeeId('guests', sha1($sessionId), $identifier, $statusCode === 200 ? $user : null); + } elseif (str_ends_with($participant, '@{$LOCAL_REMOTE_URL}') || + str_ends_with($participant, '@{$REMOTE_URL}')) { + $participant = str_replace('{$LOCAL_REMOTE_URL}', rtrim($this->localRemoteServerUrl, '/'), $participant); + $participant = str_replace('{$REMOTE_URL}', rtrim($this->remoteServerUrl, '/'), $participant); + + $attendeeId = $this->getAttendeeId('federated_users', $participant, $identifier, $statusCode === 200 ? $user : null); } else { $attendeeId = $this->getAttendeeId('users', $participant, $identifier, $statusCode === 200 ? $user : null); } diff --git a/tests/integration/features/federation/permissions.feature b/tests/integration/features/federation/permissions.feature new file mode 100644 index 00000000000..521c074b295 --- /dev/null +++ b/tests/integration/features/federation/permissions.feature @@ -0,0 +1,131 @@ +Feature: federation/permissions + + Background: + Given user "participant1" exists + And user "participant2" exists + And the following "spreed" app config is set + | federation_enabled | yes | + + Scenario: set participant permissions + Given user "participant3" exists + And user "participant1" creates room "room" (v4) + | roomType | 2 | + | roomName | room name | + And user "participant1" adds federated_user "participant2" to room "room" with 200 (v4) + 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 name | 2 | LOCAL | room | + And user "participant1" adds federated_user "participant3" to room "room" with 200 (v4) + And user "participant3" has the following invitations (v1) + | remoteServerUrl | remoteToken | state | inviterCloudId | inviterDisplayName | + | LOCAL | room | 0 | participant1@http://localhost:8080 | participant1-displayname | + And user "participant3" accepts invite to room "room" of server "LOCAL" with 200 (v1) + | id | name | type | remoteServer | remoteToken | + | LOCAL::room | room name | 2 | LOCAL | room | + When user "participant1" sets permissions for "participant2@{$LOCAL_REMOTE_URL}" in room "room" to "S" with 200 (v4) + Then user "participant2" is participant of room "LOCAL::room" (v4) + | permissions | attendeePermissions | + | CS | CS | + Then user "participant3" is participant of room "LOCAL::room" (v4) + | permissions | attendeePermissions | + | SJAVPM | D | + + Scenario: set default permissions + Given user "participant1" creates room "room" (v4) + | roomType | 2 | + | roomName | room name | + And user "participant1" adds federated_user "participant2" to room "room" with 200 (v4) + 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 name | 2 | LOCAL | room | + When user "participant1" sets default permissions for room "room" to "LM" with 200 (v4) + Then user "participant2" is participant of room "LOCAL::room" (v4) + | defaultPermissions | attendeePermissions | permissions | + | CLM | D | CLM | + + Scenario: set default permissions before federated user accepts invitation + Given user "participant1" creates room "room" (v4) + | roomType | 2 | + | roomName | room name | + And user "participant1" adds federated_user "participant2" to room "room" with 200 (v4) + And user "participant2" has the following invitations (v1) + | remoteServerUrl | remoteToken | state | inviterCloudId | inviterDisplayName | + | LOCAL | room | 0 | participant1@http://localhost:8080 | participant1-displayname | + When user "participant1" sets default permissions for room "room" to "LM" with 200 (v4) + And user "participant2" accepts invite to room "room" of server "LOCAL" with 200 (v1) + | id | name | type | remoteServer | remoteToken | + | LOCAL::room | room name | 2 | LOCAL | room | + Then user "participant2" is participant of room "LOCAL::room" (v4) + | defaultPermissions | attendeePermissions | permissions | + | CLM | D | CLM | + + Scenario: set default permissions before inviting federated user + Given user "participant1" creates room "room" (v4) + | roomType | 2 | + | roomName | room name | + When user "participant1" sets default permissions for room "room" to "M" with 200 (v4) + And user "participant1" adds federated_user "participant2" to room "room" with 200 (v4) + 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 name | 2 | LOCAL | room | + Then user "participant2" is participant of room "LOCAL::room" (v4) + | defaultPermissions | attendeePermissions | permissions | + | CM | D | CM | + + Scenario: set default permissions before inviting federated user again + Given user "participant1" creates room "room" (v4) + | roomType | 2 | + | roomName | room name | + And user "participant1" adds federated_user "participant2" to room "room" with 200 (v4) + 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" declines invite to room "room" of server "LOCAL" with 200 (v1) + When user "participant1" sets default permissions for room "room" to "M" with 200 (v4) + And user "participant1" adds federated_user "participant2" to room "room" with 200 (v4) + 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 name | 2 | LOCAL | room | + Then user "participant2" is participant of room "LOCAL::room" (v4) + | defaultPermissions | attendeePermissions | permissions | + | CM | D | CM | + + Scenario: set participant permissions after setting conversation permissions and then invite another federated user + Given user "participant3" exists + And user "participant1" creates room "room" (v4) + | roomType | 2 | + | roomName | room name | + And user "participant1" adds federated_user "participant2" to room "room" with 200 (v4) + 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 name | 2 | LOCAL | room | + And user "participant1" sets default permissions for room "room" to "AVP" with 200 (v4) + And user "participant1" sets permissions for "participant2@{$LOCAL_REMOTE_URL}" in room "room" to "S" with 200 (v4) + When user "participant1" adds federated_user "participant3" to room "room" with 200 (v4) + And user "participant3" has the following invitations (v1) + | remoteServerUrl | remoteToken | state | inviterCloudId | inviterDisplayName | + | LOCAL | room | 0 | participant1@http://localhost:8080 | participant1-displayname | + And user "participant3" accepts invite to room "room" of server "LOCAL" with 200 (v1) + | id | name | type | remoteServer | remoteToken | + | LOCAL::room | room name | 2 | LOCAL | room | + Then user "participant2" is participant of room "LOCAL::room" (v4) + | permissions | + | CS | + And user "participant3" is participant of room "LOCAL::room" (v4) + | permissions | + | CAVP | diff --git a/tests/php/Federation/FederationTest.php b/tests/php/Federation/FederationTest.php index 936f19c3b6f..94c465503e2 100644 --- a/tests/php/Federation/FederationTest.php +++ b/tests/php/Federation/FederationTest.php @@ -256,6 +256,7 @@ public function testReceiveRemoteShare(): void { $shareType = 'user'; $roomType = Room::TYPE_GROUP; $roomName = 'Room name'; + $roomDefaultPermissions = Attendee::PERMISSIONS_CUSTOM | Attendee::PERMISSIONS_CHAT; $shareWithUser = $this->createMock(IUser::class); $shareWithUserID = '10'; @@ -277,6 +278,7 @@ public function testReceiveRemoteShare(): void { 'name' => 'nctalk', 'roomType' => $roomType, 'roomName' => $roomName, + 'roomDefaultPermissions' => $roomDefaultPermissions, 'options' => [ 'sharedSecret' => $token, ], @@ -288,7 +290,7 @@ public function testReceiveRemoteShare(): void { // Test receiving federation expectations $this->federationManager->expects($this->once()) ->method('addRemoteRoom') - ->with($shareWithUser, $providerId, $roomType, $roomName, $name, $remote, $token) + ->with($shareWithUser, $providerId, $roomType, $roomName, $roomDefaultPermissions, $name, $remote, $token) ->willReturn($invite); $this->config->method('isFederationEnabled')