Skip to content
Merged
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
1 change: 1 addition & 0 deletions docs/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ Legend:
| `allowed_groups` | string[] | `[]` | Yes | 🖌️ | List of group ids that are allowed to use Talk |
| `sip_bridge_groups` | string[] | `[]` | Yes | 🖌️ | List of group ids that are allowed to enable SIP dial-in in a conversation |
| `start_conversations` | string[] | `[]` | Yes | 🖌️ | List of group ids that are allowed to create conversations |
| `start_calls_groups` | string[] | `[]` | Yes | 🖌️ | List of group ids that are allowed to start calls (everyone when empty). This also applies to moderators; guests and federated users can then never start calls. For federated conversations the host server decides. Others can still chat and join ongoing calls |
| `federation_allowed_groups` | string[] | `[]` | Yes | 🖌️ | 🏗️ *Work in progress:* List of group ids that are allowed to invite federated users into their conversations (everyone when empty) |
| `hosted-signaling-server-account` | array | `{}` | No | 🖌️ | Account information of the hosted signaling server |
| `stun_servers` | array[] | `[]` | Yes | 🖌💻️ | List of STUN servers, should be configured via the web interface or the OCC commands |
Expand Down
1 change: 1 addition & 0 deletions lib/Controller/RoomController.php
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ protected function getTalkHashHeader(): array {
$this->config->getAppValue('spreed', 'recording_servers'),
$this->config->getAppValue('spreed', 'allowed_groups'),
$this->config->getAppValue('spreed', 'start_calls'),
$this->config->getAppValue('spreed', 'start_calls_groups'),
$this->config->getAppValue('spreed', 'start_conversations'),
$this->config->getAppValue('spreed', 'default_permissions'),
$this->config->getAppValue('spreed', 'breakout_rooms'),
Expand Down
1 change: 1 addition & 0 deletions lib/Listener/GroupDeletedListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public function handle(Event $event): void {
$gid = $event->getGroup()->getGID();

$this->removeGroupFromConfig('sip_bridge_groups', $gid);
$this->removeGroupFromConfig('start_calls_groups', $gid);
$this->removeGroupFromConfig('start_conversations', $gid);
$this->removeGroupFromConfig('allowed_groups', $gid);

Expand Down
6 changes: 5 additions & 1 deletion lib/Listener/RestrictStartingCalls.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,20 @@
use OCA\Talk\Participant;
use OCA\Talk\Room;
use OCA\Talk\Service\ParticipantService;
use OCP\AppFramework\Services\IAppConfig;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\IConfig;
use OCP\IGroupManager;

/**
* @template-implements IEventListener<Event>
*/
class RestrictStartingCalls implements IEventListener {
public function __construct(
private readonly IConfig $serverConfig,
private readonly IAppConfig $appConfig,
private readonly IGroupManager $groupManager,
private readonly ParticipantService $participantService,
) {
}
Expand Down Expand Up @@ -62,7 +66,7 @@ public function handle(Event $event): void {
return;
}

if (!$event->getParticipant()->canStartCall($this->serverConfig)
if (!$event->getParticipant()->canStartCall($this->serverConfig, $this->appConfig, $this->groupManager)
&& !$this->participantService->hasActiveSessionsInCall($room)) {
throw new ForbiddenException('Can not start a call');
}
Expand Down
33 changes: 32 additions & 1 deletion lib/Participant.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@

use OCA\Talk\Model\Attendee;
use OCA\Talk\Model\Session;
use OCP\AppFramework\Services\IAppConfig;
use OCP\IConfig;
use OCP\IGroupManager;
use OCP\Server;

class Participant {
Expand Down Expand Up @@ -86,7 +88,7 @@ public function hasModeratorPermissions(bool $guestModeratorAllowed = true): boo
return \in_array($participantType, [self::OWNER, self::MODERATOR, self::GUEST_MODERATOR], true);
}

public function canStartCall(IConfig $config): bool {
public function canStartCall(IConfig $config, IAppConfig $appConfig, IGroupManager $groupManager): bool {
if ($this->room->getType() === Room::TYPE_NOTE_TO_SELF) {
return false;
}
Expand All @@ -101,6 +103,10 @@ public function canStartCall(IConfig $config): bool {
return false;
}

if (!$this->isAllowedToStartCallByGroup($appConfig, $groupManager)) {
return false;
}

if ($defaultStartCall === Room::START_CALL_EVERYONE) {
return true;
}
Expand All @@ -116,6 +122,31 @@ public function canStartCall(IConfig $config): bool {
return false;
}

protected function isAllowedToStartCallByGroup(IAppConfig $appConfig, IGroupManager $groupManager): bool {
$allowedGroups = $appConfig->getAppValueArray('start_calls_groups');
if (empty($allowedGroups)) {
return true;
}

if ($this->room->isFederatedConversation()) {
// The host server of the conversation decides who can start a call
return true;
}

if ($this->attendee->getActorType() !== Attendee::ACTOR_USERS) {
// Guests, email guests and federated users can never be a member of a local group
return false;
}

foreach ($allowedGroups as $groupId) {
if ($groupManager->isInGroup($this->attendee->getActorId(), $groupId)) {
return true;
}
}

return false;
}

public function getPermissions(): int {
$permissions = $this->getPermissionsFromFallbackChain();

Expand Down
4 changes: 3 additions & 1 deletion lib/Service/RoomFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Comments\IComment;
use OCP\IConfig;
use OCP\IGroupManager;
use OCP\IL10N;
use OCP\IUser;
use OCP\IUserManager;
Expand All @@ -45,6 +46,7 @@ public function __construct(
private readonly ChatManager $chatManager,
private readonly MessageParser $messageParser,
private readonly IConfig $serverConfig,
private readonly IGroupManager $groupManager,
private readonly ITimeFactory $timeFactory,
private readonly IAppManager $appManager,
private readonly IManager $userStatusManager,
Expand Down Expand Up @@ -387,7 +389,7 @@ public function formatRoomV4(
return $roomData;
}

$roomData['canStartCall'] = $currentParticipant->canStartCall($this->serverConfig)
$roomData['canStartCall'] = $currentParticipant->canStartCall($this->serverConfig, $this->appConfig, $this->groupManager)
|| ($room->getType() === Room::TYPE_PUBLIC
&& $room->getObjectType() === Room::OBJECT_TYPE_VIDEO_VERIFICATION);

Expand Down
3 changes: 3 additions & 0 deletions lib/Settings/Admin/AdminSettings.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ protected function initGeneralSettings(): void {
protected function initAllowedGroups(): void {
$this->initialState->provideInitialState('start_calls', (int)$this->serverConfig->getAppValue('spreed', 'start_calls', (string)Room::START_CALL_EVERYONE));

$groups = $this->getGroupDetailsArray($this->appConfig->getAppValueArray('start_calls_groups'), 'start_calls_groups');
$this->initialState->provideInitialState('start_calls_groups', $groups);

$groups = $this->getGroupDetailsArray($this->talkConfig->getAllowedConversationsGroupIds(), 'start_conversations');
$this->initialState->provideInitialState('start_conversations', $groups);

Expand Down
63 changes: 61 additions & 2 deletions src/components/AdminSettings/AllowedGroups.vue
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,32 @@
{{ saveLabelStartConversations }}
</NcButton>

<NcSelect
v-model="canStartCallsGroups"
inputId="allow_groups_start_call"
:inputLabel="t('spreed', 'Limit starting calls to certain groups')"
name="allow_groups_start_call"
class="form__select"
:options="groups"
:placeholder="t('spreed', 'Limit starting calls to certain groups')"
:disabled="loading"
:multiple="true"
:searchable="true"
:tagWidth="60"
:loading="loadingGroups"
:showNoOptions="false"
keepOpen
trackBy="id"
label="displayname"
noWrap
@search="debounceSearchGroup" />
<NcButton
variant="primary"
:disabled="loading"
@click="saveStartCallsGroups">
{{ saveLabelStartCallsGroups }}
</NcButton>

<NcSelect
v-model="startCalls"
inputId="start_calls"
Expand All @@ -84,6 +110,12 @@
:disabled="loading || loadingStartCalls"
@update:modelValue="saveStartCalls" />
</div>
<p>
<em>{{ t('spreed', 'When at least one group is selected, only members of these groups can start a call. This also applies to moderators, while guests and federated users are never members of these groups. Everyone can still start a call when no group is selected.') }}</em>
</p>
<p>
<em>{{ t('spreed', 'For conversations hosted on a federated server, the configuration of the host server applies instead.') }}</em>
</p>
<p>
<em>{{ t('spreed', 'When a call has started, everyone with access to the conversation can join the call.') }}</em>
</p>
Expand Down Expand Up @@ -122,8 +154,10 @@ export default {
groups: [],
allowedGroups: [],
canStartConversations: [],
canStartCallsGroups: [],
saveLabelAllowedGroups: t('spreed', 'Save changes'),
saveLabelStartConversations: t('spreed', 'Save changes'),
saveLabelStartCallsGroups: t('spreed', 'Save changes'),

startCallOptions,
startCalls: startCallOptions[0],
Expand All @@ -140,12 +174,16 @@ export default {
this.canStartConversations = loadState('spreed', 'start_conversations', []).sort(function(a, b) {
return a.displayname.localeCompare(b.displayname)
})
this.canStartCallsGroups = loadState('spreed', 'start_calls_groups', []).sort(function(a, b) {
return a.displayname.localeCompare(b.displayname)
})
this.startCalls = startCallOptions[parseInt(loadState('spreed', 'start_calls'))]

// Make a unique list with the groups we know from allowedGroups and canStartConversations
// Make a unique list with the groups we know from allowedGroups,
// canStartConversations and canStartCallsGroups
// Unique checking is done by turning the group objects (with id and name)
// into json strings and afterwards back again
const mergedGroups = Array.from(new Set(this.allowedGroups.concat(this.canStartConversations)
const mergedGroups = Array.from(new Set(this.allowedGroups.concat(this.canStartConversations).concat(this.canStartCallsGroups)
.map((g) => JSON.stringify(g)))).map((g) => JSON.parse(g))

this.groups = mergedGroups.sort(function(a, b) {
Expand Down Expand Up @@ -223,6 +261,27 @@ export default {
})
},

saveStartCallsGroups() {
this.loading = true
this.loadingGroups = true
this.saveLabelStartCallsGroups = t('spreed', 'Saving …')

const groups = this.canStartCallsGroups.map((group) => {
return group.id
})

OCP.AppConfig.setValue('spreed', 'start_calls_groups', JSON.stringify(groups), {
success: () => {
this.loading = false
this.loadingGroups = false
this.saveLabelStartCallsGroups = t('spreed', 'Saved!')
setTimeout(() => {
this.saveLabelStartCallsGroups = t('spreed', 'Save changes')
}, 5000)
},
})
},

saveStartCalls() {
this.loadingStartCalls = true

Expand Down
37 changes: 37 additions & 0 deletions tests/integration/features/callapi/limit-starting-calls.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
Feature: callapi/limit-starting-calls
Background:
Given user "participant1" exists
Given user "participant2" exists
Given group "group1" exists

Scenario: User (even as room owner) can not start a call while not being a member of the allowed groups
Given the following "spreed" app config is set
| start_calls_groups | ["group1"] |
Given user "participant1" creates room "room" (v4)
| roomType | 3 |
| roomName | room |
And user "participant1" joins room "room" with 200 (v4)
Then user "participant1" joins call "room" with 403 (v4)
Given user "participant1" is member of group "group1"
Then user "participant1" joins call "room" with 200 (v4)

Scenario: User that is not a member of the allowed groups can join an ongoing call
Given the following "spreed" app config is set
| start_calls_groups | ["group1"] |
And user "participant1" is member of group "group1"
Given user "participant1" creates room "room" (v4)
| roomType | 3 |
| roomName | room |
And user "participant1" adds user "participant2" to room "room" with 200 (v4)
And user "participant1" joins room "room" with 200 (v4)
And user "participant2" joins room "room" with 200 (v4)
Then user "participant2" joins call "room" with 403 (v4)
When user "participant1" joins call "room" with 200 (v4)
Then user "participant2" joins call "room" with 200 (v4)

Scenario: Without the app config being set every user can start a call
Given user "participant1" creates room "room" (v4)
| roomType | 3 |
| roomName | room |
And user "participant1" joins room "room" with 200 (v4)
Then user "participant1" joins call "room" with 200 (v4)
1 change: 1 addition & 0 deletions tests/php/ConfigTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -411,4 +411,5 @@ public function testSignalingTicketV2Anonymous(string $algo): void {
$this->assertEquals($now, $decoded->iat);
$this->assertEquals('https://domain.invalid/nextcloud', $decoded->iss);
}

}
10 changes: 8 additions & 2 deletions tests/php/Listener/RestrictStartingCallsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
use OCA\Talk\Participant;
use OCA\Talk\Room;
use OCA\Talk\Service\ParticipantService;
use OCP\AppFramework\Services\IAppConfig;
use OCP\IConfig;
use OCP\IGroupManager;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\MockObject\MockObject;
Expand All @@ -23,15 +25,19 @@
#[Group('DB')]
class RestrictStartingCallsTest extends TestCase {
protected IConfig&MockObject $serverConfig;
protected IAppConfig&MockObject $appConfig;
protected IGroupManager&MockObject $groupManager;
protected ParticipantService&MockObject $participantService;
protected ?RestrictStartingCalls $listener = null;

public function setUp(): void {
parent::setUp();

$this->serverConfig = $this->createMock(IConfig::class);
$this->appConfig = $this->createMock(IAppConfig::class);
$this->groupManager = $this->createMock(IGroupManager::class);
$this->participantService = $this->createMock(ParticipantService::class);
$this->listener = new RestrictStartingCalls($this->serverConfig, $this->participantService);
$this->listener = new RestrictStartingCalls($this->serverConfig, $this->appConfig, $this->groupManager, $this->participantService);
}

public static function dataCheckStartCallPermissions(): array {
Expand All @@ -55,7 +61,7 @@ public function testCheckStartCallPermissions(int $roomType, string $roomObjectT

$participant = $this->createMock(Participant::class);
$participant->method('canStartCall')
->with($this->serverConfig)
->with($this->serverConfig, $this->appConfig, $this->groupManager)
->willReturn($canStart);

$this->participantService->method('hasActiveSessionsInCall')
Expand Down
Loading
Loading