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
129 changes: 129 additions & 0 deletions lib/Authenticator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Talk;

use OCA\Talk\Exceptions\ParticipantNotFoundException;
use OCA\Talk\Exceptions\RoomNotFoundException;
use OCA\Talk\Model\Attendee;
use OCP\Federation\ICloudIdManager;
use OCP\IRequest;

class Authenticator {
protected bool $resolved = false;
protected bool $isFederationRequest = false;
protected bool $isAuthenticatedEmailGuest = false;
protected string $actorType = '';
protected string $actorId = '';
protected string $accessToken = '';
protected ?Room $room = null;
protected ?Participant $participant = null;

public function __construct(
protected readonly IRequest $request,
protected readonly ICloudIdManager $cloudIdManager,
protected readonly TalkSession $talkSession,
) {
}

protected function resolve(): void {
if ($this->resolved) {
return;
}
$this->resolved = true;

if ((bool)$this->request->getHeader('x-nextcloud-federation')) {
$authUser = urldecode($this->request->server['PHP_AUTH_USER'] ?? '');
try {
$cloudId = $this->cloudIdManager->resolveCloudId($authUser);
$this->isFederationRequest = true;
$this->actorType = Attendee::ACTOR_FEDERATED_USERS;
$this->actorId = $cloudId->getId();
$this->accessToken = $this->request->server['PHP_AUTH_PW'] ?? '';
return;
} catch (\InvalidArgumentException) {
// Fall through to other authentication shapes
}
}

$token = $this->request->getParam('token');
if (is_string($token) && $token !== '') {
$emailActorId = $this->talkSession->getAuthedEmailActorIdForRoom($token);
if ($emailActorId !== null) {
$this->isAuthenticatedEmailGuest = true;
$this->actorType = Attendee::ACTOR_EMAILS;
$this->actorId = $emailActorId;
}
}
}

public function isFederationRequest(): bool {
$this->resolve();
return $this->isFederationRequest;
}

public function isAuthenticatedEmailGuest(): bool {
$this->resolve();
return $this->isAuthenticatedEmailGuest;
}

public function isAuthenticatedRequest(): bool {
$this->resolve();
return $this->isFederationRequest || $this->isAuthenticatedEmailGuest;
}

public function getActorType(): string {
$this->resolve();
return $this->actorType;
}

public function getActorId(): string {
$this->resolve();
return $this->actorId;
}

/**
* Federation-only alias for {@see getActorId()}. Returns an empty string
* when the current request is not a federation request, even if another
* authenticated actor (e.g. email guest) is present.
*/
public function getCloudId(): string {
return $this->isFederationRequest() ? $this->getActorId() : '';
}

public function getAccessToken(): string {
$this->resolve();
return $this->accessToken;
}

public function authenticated(Room $room, Participant $participant): void {
$this->room = $room;
$this->participant = $participant;
}

/**
* @throws RoomNotFoundException
*/
public function getRoom(): Room {
if ($this->room === null) {
throw new RoomNotFoundException();
}
return $this->room;
}

/**
* @throws ParticipantNotFoundException
*/
public function getParticipant(): Participant {
if ($this->participant === null) {
throw new ParticipantNotFoundException();
}
return $this->participant;
}
}
2 changes: 1 addition & 1 deletion lib/Chat/Parser/SystemMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@

use OCA\Circles\CirclesManager;
use OCA\DAV\CardDAV\PhotoCache;
use OCA\Talk\Authenticator;
use OCA\Talk\Chat\ChatManager;
use OCA\Talk\Events\MessageParseEvent;
use OCA\Talk\Events\OverwritePublicSharePropertiesEvent;
use OCA\Talk\Exceptions\ParticipantNotFoundException;
use OCA\Talk\Federation\Authenticator;
use OCA\Talk\Model\Attendee;
use OCA\Talk\Model\Message;
use OCA\Talk\Participant;
Expand Down
1 change: 0 additions & 1 deletion lib/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@

use OCA\Talk\AppInfo\Application;
use OCA\Talk\Events\BeforeTurnServersGetEvent;
use OCA\Talk\Federation\Authenticator;
use OCA\Talk\Model\Attendee;
use OCA\Talk\Service\RecordingService;
use OCA\Talk\Settings\UserPreference;
Expand Down
2 changes: 1 addition & 1 deletion lib/Controller/CallController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@

namespace OCA\Talk\Controller;

use OCA\Talk\Authenticator;
use OCA\Talk\Config;
use OCA\Talk\Exceptions\DialOutFailedException;
use OCA\Talk\Exceptions\ParticipantNotFoundException;
use OCA\Talk\Federation\Authenticator;
use OCA\Talk\Manager;
use OCA\Talk\Middleware\Attribute\FederationSupported;
use OCA\Talk\Middleware\Attribute\RequireCallEnabled;
Expand Down
8 changes: 4 additions & 4 deletions lib/Controller/ChatController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
namespace OCA\Talk\Controller;

use OCA\Talk\AppInfo\Application;
use OCA\Talk\Authenticator;
use OCA\Talk\Chat\AutoComplete\SearchPlugin;
use OCA\Talk\Chat\AutoComplete\Sorter;
use OCA\Talk\Chat\ChatManager;
Expand All @@ -19,7 +20,6 @@
use OCA\Talk\Exceptions\CannotReachRemoteException;
use OCA\Talk\Exceptions\ChatSummaryException;
use OCA\Talk\Exceptions\ParticipantNotFoundException;
use OCA\Talk\Federation\Authenticator;
use OCA\Talk\GuestManager;
use OCA\Talk\Manager;
use OCA\Talk\MatterbridgeManager;
Expand Down Expand Up @@ -251,8 +251,8 @@ private function resolveReplyTo(int $replyTo, string $replyToToken, string $acto
* @return list{0: Attendee::ACTOR_*, 1: string}
*/
protected function getActorInfo(string $actorDisplayName = ''): array {
$remoteCloudId = $this->federationAuthenticator->getCloudId();
if ($remoteCloudId !== '') {
if ($this->federationAuthenticator->getActorType() === Attendee::ACTOR_FEDERATED_USERS) {
$remoteCloudId = $this->federationAuthenticator->getActorId();
if ($actorDisplayName) {
$this->participantService->updateDisplayNameForActor(Attendee::ACTOR_FEDERATED_USERS, $remoteCloudId, $actorDisplayName);
}
Expand Down Expand Up @@ -1280,7 +1280,7 @@ public function getMessageContext(
}
}

$currentUser = $this->userManager->get($this->userId);
$currentUser = $this->userId !== null ? $this->userManager->getExistingUser($this->userId) : null;
if ($messageId === 0) {
// Guest in a fully expired chat, no history, just loading the chat from beginning for now
$commentsHistory = $commentsFuture = [];
Expand Down
69 changes: 49 additions & 20 deletions lib/Controller/RoomController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
namespace OCA\Talk\Controller;

use OCA\DAV\CalDAV\TimezoneService;
use OCA\Talk\Authenticator;
use OCA\Talk\Capabilities;
use OCA\Talk\Config;
use OCA\Talk\Events\AAttendeeRemovedEvent;
Expand Down Expand Up @@ -36,7 +37,6 @@
use OCA\Talk\Exceptions\RoomProperty\SipConfigurationException;
use OCA\Talk\Exceptions\RoomProperty\TypeException;
use OCA\Talk\Exceptions\UnauthorizedException;
use OCA\Talk\Federation\Authenticator;
use OCA\Talk\Federation\FederationManager;
use OCA\Talk\Federation\Proxy\TalkV1\ProxyRequest;
use OCA\Talk\GuestManager;
Expand Down Expand Up @@ -456,15 +456,32 @@ public function getSingleRoom(string $token): DataResponse {
$isTalkFederation = $this->request->getHeader('x-nextcloud-federation');

if (!$isTalkFederation) {
$sessionId = $this->session->getSessionForRoom($token);
$room = $this->manager->getRoomForUserByToken($token, $this->userId, $sessionId, $includeLastMessage, $isSIPBridgeRequest);
if ($this->userId === null && $this->federationAuthenticator->isAuthenticatedEmailGuest()) {
// Authenticated email guest: the user/public-room check in
// getRoomForUserByToken() would reject non-public rooms, so
// trust the session-stored actor id and require the email
// attendee to still exist.
$room = $this->manager->getRoomByToken($token);
try {
$participant = $this->participantService->getParticipantByActor(
$room,
Attendee::ACTOR_EMAILS,
$this->federationAuthenticator->getActorId(),
);
} catch (ParticipantNotFoundException) {
throw new RoomNotFoundException();
}
} else {
$sessionId = $this->session->getSessionForRoom($token);
$room = $this->manager->getRoomForUserByToken($token, $this->userId, $sessionId, $includeLastMessage, $isSIPBridgeRequest);

try {
$participant = $this->participantService->getParticipant($room, $this->userId, $sessionId);
} catch (ParticipantNotFoundException) {
try {
$participant = $this->participantService->getParticipantBySession($room, $sessionId);
$participant = $this->participantService->getParticipant($room, $this->userId, $sessionId);
} catch (ParticipantNotFoundException) {
try {
$participant = $this->participantService->getParticipantBySession($room, $sessionId);
} catch (ParticipantNotFoundException) {
}
}
}
} else {
Expand Down Expand Up @@ -1426,13 +1443,6 @@ public function addParticipantToRoom(string $newParticipant, string $source = 'u

$this->participantService->addCircle($this->room, $circle, $participants);
} elseif ($source === 'emails') {
$data = [];
try {
$this->roomService->setType($this->room, Room::TYPE_PUBLIC);
$data = ['type' => $this->room->getType()];
} catch (TypeException) {
}

$email = strtolower($newParticipant);
$actorId = hash('sha256', $email);
try {
Expand All @@ -1442,7 +1452,7 @@ public function addParticipantToRoom(string $newParticipant, string $source = 'u
$this->guestManager->sendEmailInvitation($this->room, $participant);
}

return new DataResponse($data);
return new DataResponse([]);
} elseif ($source === 'federated_users') {
if (!$this->talkConfig->isFederationEnabled()) {
return new DataResponse(['error' => 'federation'], Http::STATUS_NOT_IMPLEMENTED);
Expand Down Expand Up @@ -2020,9 +2030,20 @@ public function markConversationAsInsensitive(): DataResponse {
])]
public function joinRoom(string $token, string $password = '', bool $force = true): DataResponse {
$sessionId = $this->session->getSessionForRoom($token);
$authenticatedEmailGuest = null;
if ($this->userId === null && $this->federationAuthenticator->isAuthenticatedEmailGuest()) {
$authenticatedEmailGuest = $this->federationAuthenticator->getActorId();
}

try {
// The participant is just joining, so enforce to not load any session
$room = $this->manager->getRoomForUserByToken($token, $this->userId, null);
// The participant is just joining, so enforce to not load any session.
// Authenticated email guests bypass the user/public-room access check;
// the email attendee lookup below is the trust anchor instead.
if ($authenticatedEmailGuest !== null) {
$room = $this->manager->getRoomByToken($token);
} else {
$room = $this->manager->getRoomForUserByToken($token, $this->userId, null);
}
} catch (RoomNotFoundException $e) {
$response = new DataResponse(null, Http::STATUS_NOT_FOUND);
$response->throttle(['token' => $token, 'action' => 'talkRoomToken']);
Expand Down Expand Up @@ -2072,8 +2093,6 @@ public function joinRoom(string $token, string $password = '', bool $force = tru
}
}

$authenticatedEmailGuest = $this->session->getAuthedEmailActorIdForRoom($token);

$headers = [];
if ($authenticatedEmailGuest !== null || $room->isFederatedConversation()
|| ($previousParticipant instanceof Participant && $previousParticipant->isGuest())) {
Expand All @@ -2095,6 +2114,12 @@ public function joinRoom(string $token, string $password = '', bool $force = tru
try {
$previousParticipant = $this->participantService->getParticipantByActor($room, Attendee::ACTOR_EMAILS, $authenticatedEmailGuest);
} catch (ParticipantNotFoundException) {
// Stale spreed-authed-email session: the email attendee was
// removed (e.g. by a moderator) since the invite link was opened.
$this->session->removeAuthedEmailActorIdForRoom($token);
$response = new DataResponse(null, Http::STATUS_NOT_FOUND);
$response->throttle(['token' => $token, 'action' => 'talkRoomToken']);
return $response;
}
}
$participant = $this->participantService->joinRoomAsNewGuest($this->roomService, $room, $password, $result['result'], $previousParticipant);
Expand Down Expand Up @@ -2544,7 +2569,11 @@ public function leaveRoom(string $token): DataResponse {
$this->session->removeSessionForRoom($token);

try {
$room = $this->manager->getRoomForUserByToken($token, $this->userId, $sessionId);
if ($this->userId === null && $this->federationAuthenticator->isAuthenticatedEmailGuest()) {
$room = $this->manager->getRoomByToken($token);
} else {
$room = $this->manager->getRoomForUserByToken($token, $this->userId, $sessionId);
}
$participant = $this->participantService->getParticipantBySession($room, $sessionId);

if ($room->isFederatedConversation()) {
Expand Down
30 changes: 24 additions & 6 deletions lib/Controller/SignalingController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@

namespace OCA\Talk\Controller;

use OCA\Talk\Authenticator;
use OCA\Talk\Config;
use OCA\Talk\Events\BeforeSignalingResponseSentEvent;
use OCA\Talk\Exceptions\ForbiddenException;
use OCA\Talk\Exceptions\ParticipantNotFoundException;
use OCA\Talk\Exceptions\RoomNotFoundException;
use OCA\Talk\Exceptions\UnauthorizedException;
use OCA\Talk\Federation\Authenticator;
use OCA\Talk\Manager;
use OCA\Talk\Model\Attendee;
use OCA\Talk\Model\Session;
Expand Down Expand Up @@ -161,6 +161,14 @@ public function getSettings(string $token = ''): DataResponse {
$this->federationAuthenticator->getCloudId()
);
$this->federationAuthenticator->authenticated($room, $participant);
} elseif ($token !== '' && $this->userId === null && $this->federationAuthenticator->isAuthenticatedEmailGuest()) {
$room = $this->manager->getRoomByToken($token);
$participant = $this->participantService->getParticipantByActor(
$room,
Attendee::ACTOR_EMAILS,
$this->federationAuthenticator->getActorId(),
);
$this->federationAuthenticator->authenticated($room, $participant);
} elseif ($token !== '') {
$room = $this->manager->getRoomForUserByToken($token, $this->userId);
} elseif ($this->userId !== null || $isRecordingRequest) {
Expand Down Expand Up @@ -600,11 +608,21 @@ public function pullMessages(string $token): DataResponse {

// Was the session killed or the complete conversation?
try {
$room = $this->manager->getRoomForUserByToken($token, $this->userId);
if ($this->userId) {
// For logged in users we check if they are still part of the public conversation,
// if not they were removed instead of having a conflict.
$this->participantService->getParticipant($room, $this->userId, false);
if ($this->userId === null && $this->federationAuthenticator->isAuthenticatedEmailGuest()) {
$room = $this->manager->getRoomByToken($token);
// Verify the email attendee still exists; otherwise treat as removed.
$this->participantService->getParticipantByActor(
$room,
Attendee::ACTOR_EMAILS,
$this->federationAuthenticator->getActorId(),
);
} else {
$room = $this->manager->getRoomForUserByToken($token, $this->userId);
if ($this->userId) {
// For logged in users we check if they are still part of the public conversation,
// if not they were removed instead of having a conflict.
$this->participantService->getParticipant($room, $this->userId, false);
}
}

// Session was killed, make the UI redirect to an error
Expand Down
Loading
Loading