Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add option to disable end-to-end encryption to allow legacy clients #14006

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions appinfo/routes/routesRoomController.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@
['name' => 'Room#setLobby', 'url' => '/api/{apiVersion}/room/{token}/webinar/lobby', 'verb' => 'PUT', 'requirements' => $requirementsWithToken],
/** @see \OCA\Talk\Controller\RoomController::setSIPEnabled() */
['name' => 'Room#setSIPEnabled', 'url' => '/api/{apiVersion}/room/{token}/webinar/sip', 'verb' => 'PUT', 'requirements' => $requirementsWithToken],
/** @see \OCA\Talk\Controller\RoomController::setEncryptionEnabled() */
['name' => 'Room#setEncryptionEnabled', 'url' => '/api/{apiVersion}/room/{token}/webinar/encryption', 'verb' => 'PUT', 'requirements' => $requirementsWithToken],
/** @see \OCA\Talk\Controller\RoomController::setRecordingConsent() */
['name' => 'Room#setRecordingConsent', 'url' => '/api/{apiVersion}/room/{token}/recording-consent', 'verb' => 'PUT', 'requirements' => $requirementsWithToken],
/** @see \OCA\Talk\Controller\RoomController::setMessageExpiration() */
Expand Down
29 changes: 29 additions & 0 deletions lib/Controller/RoomController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2369,6 +2369,35 @@ public function setSIPEnabled(int $state): DataResponse {
return new DataResponse($this->formatRoom($this->room, $this->participant));
}

/**
* Update end-to-end encryption enabled state
*
* @param bool $state New state
* @psalm-param Webinary::SIP_* $state
* @return DataResponse<Http::STATUS_OK, TalkRoom, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED|Http::STATUS_FORBIDDEN|Http::STATUS_PRECONDITION_FAILED, array{error: 'config'}, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'breakout-room'|'token'|'type'|'value'}, array{}>
*
* 200: End-to-end encryption enabled state updated successfully
* 400: Updating end-to-end encryption enabled state is not possible
* 401: User not found
* 403: Missing permissions to update end-to-end encryption enabled state
*/
#[NoAdminRequired]
#[RequireModeratorParticipant]
public function setEncryptionEnabled(bool $enabled): DataResponse {
$user = $this->userManager->get($this->userId);
if (!$user instanceof IUser) {
return new DataResponse(['error' => 'config'], Http::STATUS_UNAUTHORIZED);
}

try {
$this->roomService->setEncryptionEnabled($this->room, $enabled);
} catch (SipConfigurationException $e) {
return new DataResponse(['error' => $e->getReason()], Http::STATUS_BAD_REQUEST);
}

return new DataResponse($this->formatRoom($this->room, $this->participant));
}

/**
* Set recording consent requirement for this conversation
*
Expand Down
1 change: 1 addition & 0 deletions lib/Controller/SignalingController.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@
/**
* Get the signaling settings
*
* @param string $token Token of the room

Check failure on line 110 in lib/Controller/SignalingController.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis

InvalidReturnType

lib/Controller/SignalingController.php:110:13: InvalidReturnType: The declared return type 'OCP\AppFramework\Http\DataResponse<200, array{federation: array{helloAuthParams: array{token: string}, nextcloudServer: string, roomId: string, server: string}|null, helloAuthParams: array{'1.0': array{ticket: string, userid: null|string}, '2.0': array{token: string}}, hideWarning: bool, server: string, signalingMode: string, sipDialinInfo: string, stunservers: list<array{urls: list<string>}>, ticket: string, turnservers: list<array{credential: mixed, urls: list<string>, username: string}>, userId: null|string}, array<never, never>>|OCP\AppFramework\Http\DataResponse<401|404, null, array<never, never>>' for OCA\Talk\Controller\SignalingController::getSettings is incorrect, got 'OCP\AppFramework\Http\DataResponse<200, array{encrypted: bool, federation: array{helloAuthParams: array{token: string}, nextcloudServer: string, roomId: string, server: string}|null, helloAuthParams: array{'1.0': array{ticket: string, userid: null|string}, '2.0': array{token: string}}, hideWarning: bool, server: string, signalingMode: string, sipDialinInfo: string, stunservers: list{array{urls: list<non-falsy-string>}}, ticket: string, turnservers: list<array{credential: string, urls: non-empty-list<non-falsy-string>, username: string}>, userId: null|string}, array<never, never>>|OCP\AppFramework\Http\DataResponse<401|404, null, array<never, never>>' (see https://psalm.dev/011)
* @return DataResponse<Http::STATUS_OK, TalkSignalingSettings, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED|Http::STATUS_NOT_FOUND, null, array{}>
*
* 200: Signaling settings returned
Expand Down Expand Up @@ -218,8 +218,9 @@
'stunservers' => $stun,
'turnservers' => $turn,
'sipDialinInfo' => $this->talkConfig->isSIPConfigured() ? $this->talkConfig->getDialInInfo() : '',
'encrypted' => $room->getEncryptionEnabled(),
];

Check failure on line 223 in lib/Controller/SignalingController.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis

InvalidReturnStatement

lib/Controller/SignalingController.php:223:10: InvalidReturnStatement: The inferred type 'OCP\AppFramework\Http\DataResponse<200, array{encrypted: bool, federation: array{helloAuthParams: array{token: string}, nextcloudServer: string, roomId: string, server: string}|null, helloAuthParams: array{'1.0': array{ticket: string, userid: null|string}, '2.0': array{token: string}}, hideWarning: bool, server: string, signalingMode: string, sipDialinInfo: string, stunservers: list{array{urls: list<non-falsy-string>}}, ticket: string, turnservers: list<array{credential: string, urls: non-empty-list<non-falsy-string>, username: string}>, userId: null|string}, array<never, never>>' does not match the declared return type 'OCP\AppFramework\Http\DataResponse<200, array{federation: array{helloAuthParams: array{token: string}, nextcloudServer: string, roomId: string, server: string}|null, helloAuthParams: array{'1.0': array{ticket: string, userid: null|string}, '2.0': array{token: string}}, hideWarning: bool, server: string, signalingMode: string, sipDialinInfo: string, stunservers: list<array{urls: list<string>}>, ticket: string, turnservers: list<array{credential: mixed, urls: list<string>, username: string}>, userId: null|string}, array<never, never>>|OCP\AppFramework\Http\DataResponse<401|404, null, array<never, never>>' for OCA\Talk\Controller\SignalingController::getSettings (see https://psalm.dev/128)
return new DataResponse($data);
}

Expand Down
9 changes: 5 additions & 4 deletions lib/Events/ARoomModifiedEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ abstract class ARoomModifiedEvent extends ARoomEvent {
public const PROPERTY_READ_ONLY = 'readOnly';
public const PROPERTY_RECORDING_CONSENT = 'recordingConsent';
public const PROPERTY_SIP_ENABLED = 'sipEnabled';
public const PROPERTY_ENCRYPTION_ENABLED = 'encryptionEnabled';
public const PROPERTY_TYPE = 'type';

/**
Expand All @@ -39,8 +40,8 @@ abstract class ARoomModifiedEvent extends ARoomEvent {
public function __construct(
Room $room,
protected string $property,
protected \DateTime|string|int|null $newValue,
protected \DateTime|string|int|null $oldValue = null,
protected \DateTime|string|int|bool|null $newValue,
protected \DateTime|string|int|bool|null $oldValue = null,
protected ?Participant $actor = null,
) {
parent::__construct($room);
Expand All @@ -50,11 +51,11 @@ public function getProperty(): string {
return $this->property;
}

public function getNewValue(): \DateTime|string|int|null {
public function getNewValue(): \DateTime|string|int|bool|null {
return $this->newValue;
}

public function getOldValue(): \DateTime|string|int|null {
public function getOldValue(): \DateTime|string|int|bool|null {
return $this->oldValue;
}

Expand Down
2 changes: 2 additions & 0 deletions lib/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ public function createRoomObjectFromData(array $data): Room {
'message_expiration' => 0,
'lobby_state' => 0,
'sip_enabled' => 0,
'encrypted' => false,
'assigned_hpb' => null,
'token' => '',
'name' => '',
Expand Down Expand Up @@ -167,6 +168,7 @@ public function createRoomObject(array $row): Room {
(int)$row['message_expiration'],
(int)$row['lobby_state'],
(int)$row['sip_enabled'],
(bool)$row['encrypted'],
$assignedSignalingServer,
(string)$row['token'],
(string)$row['name'],
Expand Down
39 changes: 39 additions & 0 deletions lib/Migration/Version21000Date20241212134329.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

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

namespace OCA\Talk\Migration;

use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;

class Version21000Date20241212134329 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
*
* @return ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();

$table = $schema->getTable('talk_rooms');
if (!$table->hasColumn('encrypted')) {
$table->addColumn('encrypted', Types::BOOLEAN, [
'notnull' => false,
'default' => true,
]);
}

return $schema;
}
}
1 change: 1 addition & 0 deletions lib/Model/SelectHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public function selectRoomsTable(IQueryBuilder $query, string $alias = 'r'): voi
->addSelect($alias . 'read_only')
->addSelect($alias . 'lobby_state')
->addSelect($alias . 'sip_enabled')
->addSelect($alias . 'encrypted')
->addSelect($alias . 'assigned_hpb')
->addSelect($alias . 'token')
->addSelect($alias . 'name')
Expand Down
10 changes: 10 additions & 0 deletions lib/Room.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ public function __construct(
private int $messageExpiration,
private int $lobbyState,
private int $sipEnabled,
private bool $encryptionEnabled,
private ?int $assignedSignalingServer,
private string $token,
private string $name,
Expand Down Expand Up @@ -223,6 +224,14 @@ public function setSIPEnabled(int $sipEnabled): void {
$this->sipEnabled = $sipEnabled;
}

public function getEncryptionEnabled(): bool {
return $this->encryptionEnabled;
}

public function setEncryptionEnabled(bool $encryptionEnabled): void {
$this->encryptionEnabled = $encryptionEnabled;
}

public function getAssignedSignalingServer(): ?int {
return $this->assignedSignalingServer;
}
Expand Down Expand Up @@ -432,6 +441,7 @@ public function getPropertiesForSignaling(string $userId, bool $roomModified = t
'listable' => $this->getListable(),
'active-since' => $this->getActiveSince(),
'sip-enabled' => $this->getSIPEnabled(),
'encrypted' => $this->getEncryptionEnabled(),
];

if ($roomModified) {
Expand Down
3 changes: 3 additions & 0 deletions lib/Service/RoomFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
'lastPing' => 0,
'sessionId' => '0',
'sipEnabled' => Webinary::SIP_DISABLED,
'encrypted' => false,
'actorType' => '',
'actorId' => '',
'attendeeId' => 0,
Expand Down Expand Up @@ -164,7 +165,7 @@
|| ($isListingBreakoutRooms && !$currentParticipant instanceof Participant)
|| ($room->getListable() !== Room::LISTABLE_NONE && !$currentParticipant instanceof Participant)
) {
return array_merge($roomData, [

Check failure on line 168 in lib/Service/RoomFormatter.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis

InvalidReturnStatement

lib/Service/RoomFormatter.php:168:11: InvalidReturnStatement: The inferred type 'array{actorId: '', actorType: '', attendeeId: 0, attendeePermissions: 1, attendeePin: '', avatarVersion: string, breakoutRoomMode: int, breakoutRoomStatus: int, callFlag: int, callPermissions: 1, callRecording: int, callStartTime: int, canDeleteConversation: false, canEnableSIP: false, canLeaveConversation: false, canStartCall: false, defaultPermissions: 1, description: string, displayName: string, encrypted: bool, hasCall: bool, hasPassword: bool, id: int, isArchived: false, isCustomAvatar: bool, isFavorite: false, lastActivity: int, lastCommonReadMessage: 0, lastPing: 0, lastReadMessage: 0, listable: int, lobbyState: int, lobbyTimer: int, mentionPermissions: 0, messageExpiration: 0, name: string, notificationCalls: 0, notificationLevel: 3, objectId: string, objectType: string, participantFlags: 0, participantType: 4, permissions: 1, readOnly: int, recordingConsent: 0|1|2, sessionId: '0', sipEnabled: int, token: string, type: -1|1|2|3|4|5|6, unreadMention: false, unreadMentionDirect: false, unreadMessages: 0}' does not match the declared return type 'array{actorId: string, actorType: string, attendeeId: int, attendeePermissions: int, attendeePin: null|string, avatarVersion: string, breakoutRoomMode: int, breakoutRoomStatus: int, callFlag: int, callPermissions: int, callRecording: int, callStartTime: int, canDeleteConversation: bool, canEnableSIP: bool, canLeaveConversation: bool, canStartCall: bool, defaultPermissions: int, description: string, displayName: string, hasCall: bool, hasPassword: bool, id: int, invitedActorId?: string, isArchived: bool, isCustomAvatar: bool, isFavorite: bool, lastActivity: int, lastCommonReadMessage: int, lastMessage?: array{actorDisplayName: string, actorId: string, actorType: string, deleted?: true, expirationTimestamp: int, id?: int, isReplyable?: bool, lastEditActorDisplayName?: string, lastEditActorId?: string, lastEditActorType?: string, lastEditTimestamp?: int, markdown?: bool, message: string, messageParameters: array<string, array{'call-type'?: 'group'|'one2one'|'public', 'icon-url'?: string, 'message-id'?: string, 'preview-available'?: 'no'|'yes', assignable?: '0'|'1', blurhash?: string, boardname?: string, conversation?: string, description?: string, etag?: string, height?: string, id: string, latitude?: string, link?: string, longitude?: string, mimetype?: string, mtime?: string, name: string, path?: string, permissions?: string, server?: string, size?: string, stackname?: string, thumb?: string, type: string, visibility?: '0'|'1', website?: string, width?: string}>, messageType: string, reactions?: array<string, int>|stdClass, reactionsSelf?: list<string>, referenceId?: string, silent?: bool, systemMessage: string, timestamp?: int, token?: string}, lastPing: int, lastReadMessage: int, listable: int, lobbyState: int, lobbyTimer: int, mentionPermissions: int, messageExpiration: int, name: string, notificationCalls: int, notificationLevel: int, objectId: string, objectType: string, participantFlags: int, participantType: int, permissions: int, readOnly: int, recordingConsent: int, remoteServer?: string, remoteToken?: string, sessionId: string, sipEnabled: int, status?: string, statusClearAt?: int|null, statusIcon?: null|string, statusMessage?: null|string, token: string, type: int, unreadMention: bool, unreadMentionDirect: bool, unreadMessages: int}' for OCA\Talk\Service\RoomFormatter::formatRoomV4 due to additional array shape fields (encrypted) (see https://psalm.dev/128)
'name' => $room->getName(),
'displayName' => $room->getDisplayName($isListingBreakoutRooms || $isSIPBridgeRequest || $this->userId === null ? '' : $this->userId, $isListingBreakoutRooms || $isSIPBridgeRequest),
'description' => $room->getListable() !== Room::LISTABLE_NONE ? $room->getDescription() : '',
Expand All @@ -177,6 +178,7 @@
'lobbyState' => $room->getLobbyState(),
'lobbyTimer' => $lobbyTimer,
'sipEnabled' => $room->getSIPEnabled(),
'encrypted' => $room->getEncryptionEnabled(),
'listable' => $room->getListable(),
'breakoutRoomMode' => $room->getBreakoutRoomMode(),
'breakoutRoomStatus' => $room->getBreakoutRoomStatus(),
Expand All @@ -186,7 +188,7 @@
}

if (!$currentParticipant instanceof Participant) {
return $roomData;

Check failure on line 191 in lib/Service/RoomFormatter.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis

InvalidReturnStatement

lib/Service/RoomFormatter.php:191:11: InvalidReturnStatement: The inferred type 'array{actorId: '', actorType: '', attendeeId: 0, attendeePermissions: 1, attendeePin: '', avatarVersion: string, breakoutRoomMode: 0, breakoutRoomStatus: 0, callFlag: 0, callPermissions: 1, callRecording: 0, callStartTime: 0, canDeleteConversation: false, canEnableSIP: false, canLeaveConversation: false, canStartCall: false, defaultPermissions: 1, description: '', displayName: '', encrypted: false, hasCall: false, hasPassword: bool, id: int, isArchived: false, isCustomAvatar: bool, isFavorite: false, lastActivity: 0, lastCommonReadMessage: 0, lastPing: 0, lastReadMessage: 0, listable: 0, lobbyState: 0, lobbyTimer: 0, mentionPermissions: 0, messageExpiration: 0, name: '', notificationCalls: 0, notificationLevel: 3, objectId: '', objectType: '', participantFlags: 0, participantType: 4, permissions: 1, readOnly: 0, recordingConsent: 0|1|2, sessionId: '0', sipEnabled: 0, token: string, type: -1|1|2|3|4|5|6, unreadMention: false, unreadMentionDirect: false, unreadMessages: 0}' does not match the declared return type 'array{actorId: string, actorType: string, attendeeId: int, attendeePermissions: int, attendeePin: null|string, avatarVersion: string, breakoutRoomMode: int, breakoutRoomStatus: int, callFlag: int, callPermissions: int, callRecording: int, callStartTime: int, canDeleteConversation: bool, canEnableSIP: bool, canLeaveConversation: bool, canStartCall: bool, defaultPermissions: int, description: string, displayName: string, hasCall: bool, hasPassword: bool, id: int, invitedActorId?: string, isArchived: bool, isCustomAvatar: bool, isFavorite: bool, lastActivity: int, lastCommonReadMessage: int, lastMessage?: array{actorDisplayName: string, actorId: string, actorType: string, deleted?: true, expirationTimestamp: int, id?: int, isReplyable?: bool, lastEditActorDisplayName?: string, lastEditActorId?: string, lastEditActorType?: string, lastEditTimestamp?: int, markdown?: bool, message: string, messageParameters: array<string, array{'call-type'?: 'group'|'one2one'|'public', 'icon-url'?: string, 'message-id'?: string, 'preview-available'?: 'no'|'yes', assignable?: '0'|'1', blurhash?: string, boardname?: string, conversation?: string, description?: string, etag?: string, height?: string, id: string, latitude?: string, link?: string, longitude?: string, mimetype?: string, mtime?: string, name: string, path?: string, permissions?: string, server?: string, size?: string, stackname?: string, thumb?: string, type: string, visibility?: '0'|'1', website?: string, width?: string}>, messageType: string, reactions?: array<string, int>|stdClass, reactionsSelf?: list<string>, referenceId?: string, silent?: bool, systemMessage: string, timestamp?: int, token?: string}, lastPing: int, lastReadMessage: int, listable: int, lobbyState: int, lobbyTimer: int, mentionPermissions: int, messageExpiration: int, name: string, notificationCalls: int, notificationLevel: int, objectId: string, objectType: string, participantFlags: int, participantType: int, permissions: int, readOnly: int, recordingConsent: int, remoteServer?: string, remoteToken?: string, sessionId: string, sipEnabled: int, status?: string, statusClearAt?: int|null, statusIcon?: null|string, statusMessage?: null|string, token: string, type: int, unreadMention: bool, unreadMentionDirect: bool, unreadMessages: int}' for OCA\Talk\Service\RoomFormatter::formatRoomV4 due to additional array shape fields (encrypted) (see https://psalm.dev/128)
}

$attendee = $currentParticipant->getAttendee();
Expand All @@ -210,6 +212,7 @@
'notificationCalls' => $attendee->getNotificationCalls(),
'lobbyState' => $room->getLobbyState(),
'lobbyTimer' => $lobbyTimer,
'encrypted' => $room->getEncryptionEnabled(),
'actorType' => $attendee->getActorType(),
'actorId' => $attendee->getActorId(),
'attendeeId' => $attendee->getId(),
Expand Down Expand Up @@ -278,7 +281,7 @@
// No participants and chat messages for users in the lobby.
$roomData['hasCall'] = false;
$roomData['canLeaveConversation'] = true;
return $roomData;

Check failure on line 284 in lib/Service/RoomFormatter.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis

InvalidReturnStatement

lib/Service/RoomFormatter.php:284:11: InvalidReturnStatement: The inferred type 'array{actorId: string, actorType: string, attendeeId: int, attendeePermissions: int, attendeePin: null|string, avatarVersion: string, breakoutRoomMode: int, breakoutRoomStatus: int, callFlag: int, callPermissions: 0, callRecording: int, callStartTime: int, canDeleteConversation: false, canEnableSIP: false, canLeaveConversation: true, canStartCall: false, defaultPermissions: int, description: string, displayName: string, encrypted: bool, hasCall: false, hasPassword: bool, id: int, isArchived: bool, isCustomAvatar: bool, isFavorite: bool, lastActivity: int, lastCommonReadMessage: int, lastPing: int, lastReadMessage: 0, listable: int, lobbyState: int, lobbyTimer: int, mentionPermissions: int, messageExpiration: int, name: string, notificationCalls: int, notificationLevel: int, objectId: string, objectType: string, participantFlags: int, participantType: int, permissions: int, readOnly: int, recordingConsent: 0|1|2, sessionId: string, sipEnabled: int, token: string, type: -1|1|2|3|4|5|6, unreadMention: false, unreadMentionDirect: false, unreadMessages: 0}' does not match the declared return type 'array{actorId: string, actorType: string, attendeeId: int, attendeePermissions: int, attendeePin: null|string, avatarVersion: string, breakoutRoomMode: int, breakoutRoomStatus: int, callFlag: int, callPermissions: int, callRecording: int, callStartTime: int, canDeleteConversation: bool, canEnableSIP: bool, canLeaveConversation: bool, canStartCall: bool, defaultPermissions: int, description: string, displayName: string, hasCall: bool, hasPassword: bool, id: int, invitedActorId?: string, isArchived: bool, isCustomAvatar: bool, isFavorite: bool, lastActivity: int, lastCommonReadMessage: int, lastMessage?: array{actorDisplayName: string, actorId: string, actorType: string, deleted?: true, expirationTimestamp: int, id?: int, isReplyable?: bool, lastEditActorDisplayName?: string, lastEditActorId?: string, lastEditActorType?: string, lastEditTimestamp?: int, markdown?: bool, message: string, messageParameters: array<string, array{'call-type'?: 'group'|'one2one'|'public', 'icon-url'?: string, 'message-id'?: string, 'preview-available'?: 'no'|'yes', assignable?: '0'|'1', blurhash?: string, boardname?: string, conversation?: string, description?: string, etag?: string, height?: string, id: string, latitude?: string, link?: string, longitude?: string, mimetype?: string, mtime?: string, name: string, path?: string, permissions?: string, server?: string, size?: string, stackname?: string, thumb?: string, type: string, visibility?: '0'|'1', website?: string, width?: string}>, messageType: string, reactions?: array<string, int>|stdClass, reactionsSelf?: list<string>, referenceId?: string, silent?: bool, systemMessage: string, timestamp?: int, token?: string}, lastPing: int, lastReadMessage: int, listable: int, lobbyState: int, lobbyTimer: int, mentionPermissions: int, messageExpiration: int, name: string, notificationCalls: int, notificationLevel: int, objectId: string, objectType: string, participantFlags: int, participantType: int, permissions: int, readOnly: int, recordingConsent: int, remoteServer?: string, remoteToken?: string, sessionId: string, sipEnabled: int, status?: string, statusClearAt?: int|null, statusIcon?: null|string, statusMessage?: null|string, token: string, type: int, unreadMention: bool, unreadMentionDirect: bool, unreadMessages: int}' for OCA\Talk\Service\RoomFormatter::formatRoomV4 due to additional array shape fields (encrypted) (see https://psalm.dev/128)
}

$roomData['canStartCall'] = $currentParticipant->canStartCall($this->serverConfig);
Expand Down
22 changes: 22 additions & 0 deletions lib/Service/RoomService.php
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,28 @@ public function setSIPEnabled(Room $room, int $newSipEnabled): void {
$this->dispatcher->dispatchTyped($event);
}

public function setEncryptionEnabled(Room $room, bool $newEncryptionEnabled): void {
$oldEncryptionEnabled = $room->getEncryptionEnabled();

if ($newEncryptionEnabled === $oldEncryptionEnabled) {
return;
}

$event = new BeforeRoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_ENCRYPTION_ENABLED, $newEncryptionEnabled, $oldEncryptionEnabled);
$this->dispatcher->dispatchTyped($event);

$update = $this->db->getQueryBuilder();
$update->update('talk_rooms')
->set('encrypted', $update->createNamedParameter($newEncryptionEnabled, IQueryBuilder::PARAM_BOOL))
->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)));
$update->executeStatement();

$room->setEncryptionEnabled($newEncryptionEnabled);

$event = new RoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_ENCRYPTION_ENABLED, $newEncryptionEnabled, $oldEncryptionEnabled);
$this->dispatcher->dispatchTyped($event);
}

/**
* @psalm-param RecordingService::CONSENT_REQUIRED_* $recordingConsent
* @throws RecordingConsentException When the room has an active call or the value is invalid
Expand Down
1 change: 1 addition & 0 deletions lib/Signaling/Listener.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class Listener implements IEventListener {
ARoomModifiedEvent::PROPERTY_PASSWORD,
ARoomModifiedEvent::PROPERTY_READ_ONLY,
ARoomModifiedEvent::PROPERTY_SIP_ENABLED,
ARoomModifiedEvent::PROPERTY_ENCRYPTION_ENABLED,
ARoomModifiedEvent::PROPERTY_TYPE,
];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
:name="t('spreed', 'Meeting')">
<LobbySettings :token="token" />
<SipSettings v-if="canUserEnableSIP" />
<SecuritySettings />
</NcAppSettingsSection>

<!-- Conversation permissions -->
Expand Down Expand Up @@ -129,6 +130,7 @@ import MatterbridgeSettings from './Matterbridge/MatterbridgeSettings.vue'
import MentionsSettings from './MentionsSettings.vue'
import NotificationsSettings from './NotificationsSettings.vue'
import RecordingConsentSettings from './RecordingConsentSettings.vue'
import SecuritySettings from './SecuritySettings.vue'
import SipSettings from './SipSettings.vue'

import { CALL, CONFIG, PARTICIPANT, CONVERSATION } from '../../constants.js'
Expand Down Expand Up @@ -159,6 +161,7 @@ export default {
NcCheckboxRadioSwitch,
NotificationsSettings,
RecordingConsentSettings,
SecuritySettings,
SipSettings,
},

Expand Down
84 changes: 84 additions & 0 deletions src/components/ConversationSettings/SecuritySettings.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<!--
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->

<template>
<div class="app-settings-subsection">
<h4 class="app-settings-section__subtitle">
{{ t('spreed', 'Security') }}
</h4>

<div>
<NcCheckboxRadioSwitch :checked="!hasEncryptionEnabled"
type="switch"
aria-describedby="encryption_settings_hint"
:disabled="isEncryptionLoading"
@update:checked="toggleSetting()">
{{ t('spreed', 'Disable end-to-end encryption to allow legacy clients.') }}
</NcCheckboxRadioSwitch>
</div>
</div>
</template>

<script>
import { showError, showSuccess } from '@nextcloud/dialogs'
import { t } from '@nextcloud/l10n'

import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'

export default {
name: 'SecuritySettings',

components: {
NcCheckboxRadioSwitch,
},

data() {
return {
isEncryptionLoading: false,
}
},

computed: {
token() {
return this.$store.getters.getToken()
},

conversation() {
return this.$store.getters.conversation(this.token) || this.$store.getters.dummyConversation
},

hasEncryptionEnabled() {
return this.conversation.encrypted || false
},
},

methods: {
t,
async toggleSetting() {
const enabled = !this.conversation.encrypted
try {
await this.$store.dispatch('setEncryptionEnabled', {
token: this.token,
enabled,
})
if (this.conversation.encrypted) {
showSuccess(t('spreed', 'End-to-end encryption is now enabled'))
} else {
showSuccess(t('spreed', 'End-to-end encryption is now disabled'))
}
} catch (e) {
// TODO check "precondition failed"
if (!this.conversation.encrypted) {
console.error('Error occurred when enabling end-to-end encryption', e)
showError(t('spreed', 'Error occurred when enabling end-to-end encryption'))
} else {
console.error('Error occurred when disabling end-to-end encryption', e)
showError(t('spreed', 'Error occurred when disabling end-to-end encryption'))
}
}
},
},
}
</script>
13 changes: 13 additions & 0 deletions src/services/conversationsService.js
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,18 @@ const setSIPEnabled = async function(token, newState) {
})
}

/**
* Change the end-to-end encryption enabled
*
* @param {string} token The token of the conversation to be modified
* @param {boolean} newState The new enabled state to set
*/
const setEncryptionEnabled = async function(token, newState) {
return axios.put(generateOcsUrl('apps/spreed/api/v4/room/{token}/webinar/encryption', { token }), {
enabled: newState,
})
}

/**
* Change the recording consent per conversation
*
Expand Down Expand Up @@ -372,6 +384,7 @@ export {
makeConversationPublic,
makeConversationPrivate,
setSIPEnabled,
setEncryptionEnabled,
setRecordingConsent,
changeLobbyState,
changeReadOnlyState,
Expand Down
Loading
Loading