Skip to content

Commit

Permalink
public sharing
Browse files Browse the repository at this point in the history
Signed-off-by: Hoang Pham <hoangmaths96@gmail.com>
  • Loading branch information
hweihwang committed Aug 30, 2024
1 parent 4fa5736 commit 7a37a1b
Show file tree
Hide file tree
Showing 30 changed files with 897 additions and 262 deletions.
7 changes: 7 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@

namespace OCA\Whiteboard\AppInfo;

use OCA\Files_Sharing\Event\BeforeTemplateRenderedEvent;
use OCA\Viewer\Event\LoadViewer;
use OCA\Whiteboard\Listener\AddContentSecurityPolicyListener;
use OCA\Whiteboard\Listener\BeforeTemplateRenderedListener;
use OCA\Whiteboard\Listener\LoadViewerListener;
use OCA\Whiteboard\Listener\RegisterTemplateCreatorListener;
use OCP\AppFramework\App;
Expand All @@ -21,6 +23,10 @@
use OCP\Files\Template\RegisterTemplateCreatorEvent;
use OCP\Security\CSP\AddContentSecurityPolicyEvent;

/**
* @psalm-suppress UndefinedClass
* @psalm-suppress InvalidArgument
*/
class Application extends App implements IBootstrap {
public const APP_ID = 'whiteboard';

Expand All @@ -34,6 +40,7 @@ public function register(IRegistrationContext $context): void {
$context->registerEventListener(AddContentSecurityPolicyEvent::class, AddContentSecurityPolicyListener::class);
$context->registerEventListener(LoadViewer::class, LoadViewerListener::class);
$context->registerEventListener(RegisterTemplateCreatorEvent::class, RegisterTemplateCreatorListener::class);
$context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class);
}

public function boot(IBootContext $context): void {
Expand Down
29 changes: 18 additions & 11 deletions lib/Controller/JWTController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@

namespace OCA\Whiteboard\Controller;

use OCA\Whiteboard\Service\AuthenticationService;
use Exception;
use OCA\Whiteboard\Service\Authentication\AuthenticateUserServiceFactory;
use OCA\Whiteboard\Service\ExceptionService;
use OCA\Whiteboard\Service\FileService;
use OCA\Whiteboard\Service\File\GetFileServiceFactory;
use OCA\Whiteboard\Service\JWTService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\DataResponse;
Expand All @@ -23,26 +24,32 @@
*/
final class JWTController extends Controller {
public function __construct(
IRequest $request,
private AuthenticationService $authService,
private FileService $fileService,
private JWTService $jwtService,
private ExceptionService $exceptionService
IRequest $request,
private GetFileServiceFactory $getFileServiceFactory,
private JWTService $jwtService,
private ExceptionService $exceptionService,
private AuthenticateUserServiceFactory $authenticateUserServiceFactory
) {
parent::__construct('whiteboard', $request);
}

/**
* @NoCSRFRequired
* @NoAdminRequired
* @PublicPage
*/
public function getJWT(int $fileId): DataResponse {
try {
$user = $this->authService->getAuthenticatedUser();
$file = $this->fileService->getUserFileById($user->getUID(), $fileId);
$jwt = $this->jwtService->generateJWT($user, $file, $fileId);
$publicSharingToken = $this->request->getParam('publicSharingToken');

$user = $this->authenticateUserServiceFactory->create($publicSharingToken)->authenticate();

$file = $this->getFileServiceFactory->create($user, $fileId)->getFile();

$jwt = $this->jwtService->generateJWT($user, $file);

return new DataResponse(['token' => $jwt]);
} catch (\Exception $e) {
} catch (Exception $e) {
return $this->exceptionService->handleException($e);
}
}
Expand Down
72 changes: 62 additions & 10 deletions lib/Controller/WhiteboardController.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@
namespace OCA\Whiteboard\Controller;

use Exception;
use OCA\Whiteboard\Service\AuthenticationService;
use OCA\Whiteboard\Exception\InvalidUserException;
use OCA\Whiteboard\Exception\UnauthorizedException;
use OCA\Whiteboard\Service\Authentication\GetUserFromIdServiceFactory;
use OCA\Whiteboard\Service\ConfigService;
use OCA\Whiteboard\Service\ExceptionService;
use OCA\Whiteboard\Service\FileService;
use OCA\Whiteboard\Service\File\GetFileServiceFactory;
use OCA\Whiteboard\Service\JWTService;
use OCA\Whiteboard\Service\WhiteboardContentService;
use OCP\AppFramework\ApiController;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
Expand All @@ -29,10 +33,12 @@ final class WhiteboardController extends ApiController {
public function __construct(
$appName,
IRequest $request,
private AuthenticationService $authService,
private FileService $fileService,
private GetUserFromIdServiceFactory $getUserFromIdServiceFactory,
private GetFileServiceFactory $getFileServiceFactory,
private JWTService $jwtService,
private WhiteboardContentService $contentService,
private ExceptionService $exceptionService
private ExceptionService $exceptionService,
private ConfigService $configService,
) {
parent::__construct($appName, $request);
}
Expand All @@ -42,9 +48,16 @@ public function __construct(
#[PublicPage]
public function show(int $fileId): DataResponse {
try {
$userId = $this->authService->authenticateJWT($this->request);
$file = $this->fileService->getUserFileById($userId, $fileId);
$jwt = $this->getJwtFromRequest();

$userId = $this->jwtService->getUserIdFromJWT($jwt);

$user = $this->getUserFromIdServiceFactory->create($userId)->getUser();

$file = $this->getFileServiceFactory->create($user, $fileId)->getFile();

$data = $this->contentService->getContent($file);

return new DataResponse(['data' => $data]);
} catch (Exception $e) {
return $this->exceptionService->handleException($e);
Expand All @@ -56,13 +69,52 @@ public function show(int $fileId): DataResponse {
#[PublicPage]
public function update(int $fileId, array $data): DataResponse {
try {
$this->authService->authenticateSharedToken($this->request, $fileId);
$user = $this->authService->getAndSetUser($this->request);
$file = $this->fileService->getUserFileById($user->getUID(), $fileId);
$this->validateBackendSharedToken($fileId);

$userId = $this->getUserIdFromRequest();

$user = $this->getUserFromIdServiceFactory->create($userId)->getUser();

$file = $this->getFileServiceFactory->create($user, $fileId)->getFile();

$this->contentService->updateContent($file, $data);

return new DataResponse(['status' => 'success']);
} catch (Exception $e) {
return $this->exceptionService->handleException($e);
}
}

private function getJwtFromRequest(): string {
$authHeader = $this->request->getHeader('Authorization');
if (sscanf($authHeader, 'Bearer %s', $jwt) !== 1) {
throw new UnauthorizedException();
}
return (string)$jwt;
}

private function getUserIdFromRequest(): string {
return $this->request->getHeader('X-Whiteboard-User');
}

private function validateBackendSharedToken(int $fileId): void {
$backendSharedToken = $this->request->getHeader('X-Whiteboard-Auth');
if (!$backendSharedToken || !$this->verifySharedToken($backendSharedToken, $fileId)) {
throw new InvalidUserException('Invalid backend shared token');
}
}

private function verifySharedToken(string $token, int $fileId): bool {
[$roomId, $timestamp, $signature] = explode(':', $token);

if ($roomId !== (string)$fileId) {
return false;
}

$sharedSecret = $this->configService->getWhiteboardSharedSecret();
$payload = "$roomId:$timestamp";
$expectedSignature = hash_hmac('sha256', $payload, $sharedSecret);

return hash_equals($expectedSignature, $signature);
}
}
19 changes: 19 additions & 0 deletions lib/Exception/InvalidUserException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

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

namespace OCA\Whiteboard\Exception;

use OCP\AppFramework\Http;
use RuntimeException;

final class InvalidUserException extends RuntimeException {
public function __construct(string $message = 'Invalid user') {
parent::__construct($message, Http::STATUS_BAD_REQUEST);
}
}
19 changes: 19 additions & 0 deletions lib/Exception/UnauthorizedException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

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

namespace OCA\Whiteboard\Exception;

use OCP\AppFramework\Http;
use RuntimeException;

final class UnauthorizedException extends RuntimeException {
public function __construct(string $message = 'Unauthorized') {
parent::__construct($message, Http::STATUS_UNAUTHORIZED);
}
}
43 changes: 43 additions & 0 deletions lib/Listener/BeforeTemplateRenderedListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

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


namespace OCA\Whiteboard\Listener;

use OCA\Files_Sharing\Event\BeforeTemplateRenderedEvent;
use OCP\AppFramework\Services\IInitialState;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Files\NotFoundException;

/** @template-implements IEventListener<BeforeTemplateRenderedEvent|Event> */
/**
* @psalm-suppress UndefinedClass
* @psalm-suppress MissingTemplateParam
*/
class BeforeTemplateRenderedListener implements IEventListener {
public function __construct(
private IInitialState $initialState,
) {
}

/**
* @throws NotFoundException
*/
public function handle(Event $event): void {
if (!($event instanceof BeforeTemplateRenderedEvent)) {
return;
}

$this->initialState->provideInitialState(
'file_id',
$event->getShare()->getNodeId()
);
}
}
5 changes: 5 additions & 0 deletions lib/Listener/RegisterTemplateCreatorListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
use OCP\IL10N;

/** @template-implements IEventListener<RegisterTemplateCreatorEvent|Event> */
/**
* @psalm-suppress UndefinedClass
* @psalm-suppress MissingTemplateParam
*/
final class RegisterTemplateCreatorListener implements IEventListener {
public function __construct(
private IL10N $l10n
Expand All @@ -26,6 +30,7 @@ public function handle(Event $event): void {
return;
}


$event->getTemplateManager()->registerTemplateFileCreator(function () {
$whiteboard = new TemplateFileCreator(Application::APP_ID, $this->l10n->t('New whiteboard'), '.whiteboard');
$whiteboard->addMimetype('application/vnd.excalidraw+json');
Expand Down
25 changes: 25 additions & 0 deletions lib/Model/AuthenticatedUser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

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

namespace OCA\Whiteboard\Model;

use OCP\IUser;

final class AuthenticatedUser implements User {
public function __construct(private IUser $user) {
}

public function getUID(): string {
return $this->user->getUID();
}

public function getDisplayName(): string {
return $this->user->getDisplayName();
}
}
43 changes: 43 additions & 0 deletions lib/Model/PublicSharingUser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

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

namespace OCA\Whiteboard\Model;

final class PublicSharingUser implements User {
public function __construct(
private string $publicSharingToken
) {
}

public function getUID(): string {
return $this->generateRandomUID();
}

public function getDisplayName(): string {
return $this->generateRandomDisplayName();
}

public function getPublicSharingToken(): string {
return $this->publicSharingToken;
}

private function generateRandomUID(): string {
return 'shared_' . $this->publicSharingToken . '_' . bin2hex(random_bytes(8));
}

private function generateRandomDisplayName(): string {
$adjectives = ['Anonymous', 'Mysterious', 'Incognito', 'Unknown', 'Unnamed'];
$nouns = ['User', 'Visitor', 'Guest', 'Collaborator', 'Participant'];

$adjective = $adjectives[array_rand($adjectives)];
$noun = $nouns[array_rand($nouns)];

return $adjective . ' ' . $noun;
}
}
15 changes: 15 additions & 0 deletions lib/Model/User.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

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

namespace OCA\Whiteboard\Model;

interface User {
public function getUID(): string;
public function getDisplayName(): string;
}
Loading

0 comments on commit 7a37a1b

Please sign in to comment.