Skip to content

Commit

Permalink
Merge pull request #13106 from nextcloud/backport/13092/stable30
Browse files Browse the repository at this point in the history
[stable30] fix(federation): Propagate permission changes to federated servers
  • Loading branch information
nickvergessen authored Aug 22, 2024
2 parents 7b9b367 + fded42f commit a1e3183
Show file tree
Hide file tree
Showing 9 changed files with 302 additions and 5 deletions.
2 changes: 2 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
36 changes: 36 additions & 0 deletions lib/Federation/BackendNotifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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
Expand Down
43 changes: 42 additions & 1 deletion lib/Federation/CloudFederationProviderTalk.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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();
Expand All @@ -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:
Expand Down Expand Up @@ -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<AParticipantModifiedEvent::DETAIL_*, bool>} $notification
Expand Down Expand Up @@ -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) {
Expand Down
11 changes: 11 additions & 0 deletions lib/Federation/FederationManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -42,13 +43,15 @@ 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;

public function __construct(
private Manager $manager,
private ParticipantService $participantService,
private RoomService $roomService,
private InvitationMapper $invitationMapper,
private BackendNotifier $backendNotifier,
private IManager $notificationManager,
Expand All @@ -74,6 +77,7 @@ public function addRemoteRoom(
int $remoteAttendeeId,
int $roomType,
string $roomName,
int $roomDefaultPermissions,
string $remoteToken,
string $remoteServerUrl,
#[SensitiveParameter]
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Talk\Federation\Proxy\TalkV1\Notifier;

use OCA\Talk\Events\AAttendeeRemovedEvent;
use OCA\Talk\Events\AParticipantModifiedEvent;
use OCA\Talk\Events\ParticipantModifiedEvent;
use OCA\Talk\Federation\BackendNotifier;
use OCA\Talk\Model\Attendee;
use OCA\Talk\Participant;
use OCA\Talk\Service\ParticipantService;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Federation\ICloudId;
use OCP\Federation\ICloudIdManager;

/**
* @template-implements IEventListener<Event>
*/
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(),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
9 changes: 6 additions & 3 deletions tests/integration/features/bootstrap/FeatureContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
}
Expand Down Expand Up @@ -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);
}
Expand Down
Loading

0 comments on commit a1e3183

Please sign in to comment.