mirror of
https://github.com/nextcloud/spreed.git
synced 2025-07-21 10:37:10 +00:00
feat: Add option to disable end-to-end encryption to allow legacy clients.
Signed-off-by: Joachim Bauch <bauch@struktur.de>
This commit is contained in:
@ -103,6 +103,8 @@ return [
|
||||
['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() */
|
||||
|
@ -2369,6 +2369,35 @@ class RoomController extends AEnvironmentAwareOCSController {
|
||||
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
|
||||
*
|
||||
|
@ -218,6 +218,7 @@ class SignalingController extends OCSController {
|
||||
'stunservers' => $stun,
|
||||
'turnservers' => $turn,
|
||||
'sipDialinInfo' => $this->talkConfig->isSIPConfigured() ? $this->talkConfig->getDialInInfo() : '',
|
||||
'encrypted' => $room->getEncryptionEnabled(),
|
||||
];
|
||||
|
||||
return new DataResponse($data);
|
||||
|
@ -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';
|
||||
|
||||
/**
|
||||
@ -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);
|
||||
@ -50,11 +51,11 @@ abstract class ARoomModifiedEvent extends ARoomEvent {
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -98,6 +98,7 @@ class Manager {
|
||||
'message_expiration' => 0,
|
||||
'lobby_state' => 0,
|
||||
'sip_enabled' => 0,
|
||||
'encrypted' => false,
|
||||
'assigned_hpb' => null,
|
||||
'token' => '',
|
||||
'name' => '',
|
||||
@ -167,6 +168,7 @@ class Manager {
|
||||
(int)$row['message_expiration'],
|
||||
(int)$row['lobby_state'],
|
||||
(int)$row['sip_enabled'],
|
||||
(bool)$row['encrypted'],
|
||||
$assignedSignalingServer,
|
||||
(string)$row['token'],
|
||||
(string)$row['name'],
|
||||
|
39
lib/Migration/Version21000Date20241212134329.php
Normal file
39
lib/Migration/Version21000Date20241212134329.php
Normal file
@ -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;
|
||||
}
|
||||
}
|
@ -20,6 +20,7 @@ class SelectHelper {
|
||||
->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')
|
||||
|
10
lib/Room.php
10
lib/Room.php
@ -103,6 +103,7 @@ class Room {
|
||||
private int $messageExpiration,
|
||||
private int $lobbyState,
|
||||
private int $sipEnabled,
|
||||
private bool $encryptionEnabled,
|
||||
private ?int $assignedSignalingServer,
|
||||
private string $token,
|
||||
private string $name,
|
||||
@ -223,6 +224,14 @@ class Room {
|
||||
$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;
|
||||
}
|
||||
@ -432,6 +441,7 @@ class Room {
|
||||
'listable' => $this->getListable(),
|
||||
'active-since' => $this->getActiveSince(),
|
||||
'sip-enabled' => $this->getSIPEnabled(),
|
||||
'encrypted' => $this->getEncryptionEnabled(),
|
||||
];
|
||||
|
||||
if ($roomModified) {
|
||||
|
@ -119,6 +119,7 @@ class RoomFormatter {
|
||||
'lastPing' => 0,
|
||||
'sessionId' => '0',
|
||||
'sipEnabled' => Webinary::SIP_DISABLED,
|
||||
'encrypted' => false,
|
||||
'actorType' => '',
|
||||
'actorId' => '',
|
||||
'attendeeId' => 0,
|
||||
@ -177,6 +178,7 @@ class RoomFormatter {
|
||||
'lobbyState' => $room->getLobbyState(),
|
||||
'lobbyTimer' => $lobbyTimer,
|
||||
'sipEnabled' => $room->getSIPEnabled(),
|
||||
'encrypted' => $room->getEncryptionEnabled(),
|
||||
'listable' => $room->getListable(),
|
||||
'breakoutRoomMode' => $room->getBreakoutRoomMode(),
|
||||
'breakoutRoomStatus' => $room->getBreakoutRoomStatus(),
|
||||
@ -210,6 +212,7 @@ class RoomFormatter {
|
||||
'notificationCalls' => $attendee->getNotificationCalls(),
|
||||
'lobbyState' => $room->getLobbyState(),
|
||||
'lobbyTimer' => $lobbyTimer,
|
||||
'encrypted' => $room->getEncryptionEnabled(),
|
||||
'actorType' => $attendee->getActorType(),
|
||||
'actorId' => $attendee->getActorId(),
|
||||
'attendeeId' => $attendee->getId(),
|
||||
|
@ -298,6 +298,28 @@ class RoomService {
|
||||
$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
|
||||
|
@ -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,
|
||||
];
|
||||
|
||||
|
@ -49,6 +49,7 @@
|
||||
:name="t('spreed', 'Meeting')">
|
||||
<LobbySettings :token="token" />
|
||||
<SipSettings v-if="canUserEnableSIP" />
|
||||
<SecuritySettings />
|
||||
</NcAppSettingsSection>
|
||||
|
||||
<!-- Conversation permissions -->
|
||||
@ -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'
|
||||
@ -159,6 +161,7 @@ export default {
|
||||
NcCheckboxRadioSwitch,
|
||||
NotificationsSettings,
|
||||
RecordingConsentSettings,
|
||||
SecuritySettings,
|
||||
SipSettings,
|
||||
},
|
||||
|
||||
|
84
src/components/ConversationSettings/SecuritySettings.vue
Normal file
84
src/components/ConversationSettings/SecuritySettings.vue
Normal file
@ -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>
|
@ -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
|
||||
*
|
||||
@ -372,6 +384,7 @@ export {
|
||||
makeConversationPublic,
|
||||
makeConversationPrivate,
|
||||
setSIPEnabled,
|
||||
setEncryptionEnabled,
|
||||
setRecordingConsent,
|
||||
changeLobbyState,
|
||||
changeReadOnlyState,
|
||||
|
@ -27,6 +27,7 @@ import {
|
||||
makeConversationPublic,
|
||||
makeConversationPrivate,
|
||||
setSIPEnabled,
|
||||
setEncryptionEnabled,
|
||||
setRecordingConsent,
|
||||
changeLobbyState,
|
||||
changeReadOnlyState,
|
||||
@ -684,6 +685,20 @@ const actions = {
|
||||
}
|
||||
},
|
||||
|
||||
async setEncryptionEnabled({ commit, getters }, { token, enabled }) {
|
||||
if (!getters.conversations[token]) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await setEncryptionEnabled(token, enabled)
|
||||
const conversation = Object.assign({}, getters.conversations[token], { encrypted: enabled })
|
||||
commit('addConversation', conversation)
|
||||
} catch (error) {
|
||||
console.error('Error while changing the encryption state for conversation: ', error)
|
||||
}
|
||||
},
|
||||
|
||||
async setRecordingConsent({ commit, getters }, { token, state }) {
|
||||
if (!getters.conversations[token]) {
|
||||
return
|
||||
|
@ -759,6 +759,10 @@ Signaling.Standalone.prototype.connect = function() {
|
||||
this.currentRoomToken = null
|
||||
this.nextcloudSessionId = null
|
||||
} else {
|
||||
if (this.currentRoomToken && data.room.roomid === this.currentRoomToken) {
|
||||
this._trigger('roomEncryption', [data.room.roomid, data.room.properties.encrypted || false])
|
||||
}
|
||||
|
||||
// TODO(fancycode): Only fetch properties of room that was modified.
|
||||
EventBus.emit('should-refresh-conversations')
|
||||
}
|
||||
|
@ -96,6 +96,16 @@ async function signalingGetSettingsForRecording(token, random, checksum) {
|
||||
return getSignalingSettings(token, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the encryption module.
|
||||
*
|
||||
* @param {boolean} encrypted True if encryption should be enabled, false otherwise.
|
||||
*/
|
||||
async function updateEncryption(encrypted) {
|
||||
console.debug('Setup encryption', encrypted)
|
||||
// TODO: Setup end-to-end encryption depending on "encryped" flag.
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} token The token of the conversation to connect to
|
||||
*/
|
||||
@ -122,6 +132,11 @@ async function connectSignaling(token) {
|
||||
const settings = await getSignalingSettings(token)
|
||||
console.debug('Received updated settings', settings)
|
||||
signaling.setSettings(settings)
|
||||
await updateEncryption(settings.encrypted)
|
||||
})
|
||||
|
||||
signaling.on('roomEncryption', async function(roomId, encrypted) {
|
||||
await updateEncryption(encrypted)
|
||||
})
|
||||
|
||||
signalingTypingHandler?.setSignaling(signaling)
|
||||
@ -129,6 +144,8 @@ async function connectSignaling(token) {
|
||||
signaling.setSettings(settings)
|
||||
}
|
||||
|
||||
await updateEncryption(settings.encrypted)
|
||||
|
||||
tokensInSignaling[token] = true
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user