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 23, 2024
1 parent 79c1d87 commit bd1fcf6
Show file tree
Hide file tree
Showing 15 changed files with 433 additions and 163 deletions.
4 changes: 4 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\Files\Template\RegisterTemplateCreatorEvent;
use OCP\Security\CSP\AddContentSecurityPolicyEvent;
use OCA\Files_Sharing\Event\BeforeTemplateRenderedEvent;
use OCA\Whiteboard\Listener\BeforeTemplateRenderedListener;


class Application extends App implements IBootstrap {
public const APP_ID = 'whiteboard';
Expand All @@ -34,6 +37,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);

Check failure on line 40 in lib/AppInfo/Application.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis

UndefinedClass

lib/AppInfo/Application.php:40:35: UndefinedClass: Class, interface or enum named OCA\Files_Sharing\Event\BeforeTemplateRenderedEvent does not exist (see https://psalm.dev/019)

Check failure on line 40 in lib/AppInfo/Application.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis

InvalidArgument

lib/AppInfo/Application.php:40:71: InvalidArgument: Argument 2 of OCP\AppFramework\Bootstrap\IRegistrationContext::registerEventListener expects class-string<OCP\EventDispatcher\IEventListener<OCP\EventDispatcher\Event>>, but OCA\Whiteboard\Listener\BeforeTemplateRenderedListener::class provided (see https://psalm.dev/004)
}

public function boot(IBootContext $context): void {
Expand Down
8 changes: 5 additions & 3 deletions lib/Controller/JWTController.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,14 @@ public function __construct(
/**
* @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->authService->getAuthenticatedUser($publicSharingToken);
$file = $this->fileService->getFile($user, $fileId);
$jwt = $this->jwtService->generateJWT($user, $file);
return new DataResponse(['token' => $jwt]);
} catch (\Exception $e) {
return $this->exceptionService->handleException($e);
Expand Down
30 changes: 22 additions & 8 deletions lib/Controller/WhiteboardController.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,27 @@
use OCA\Whiteboard\Service\AuthenticationService;
use OCA\Whiteboard\Service\ExceptionService;
use OCA\Whiteboard\Service\FileService;
use OCA\Whiteboard\Service\JWTService;
use OCA\Whiteboard\Service\WhiteboardContentService;
use OCP\AppFramework\ApiController;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\DataResponse;
use OCP\IRequest;
use RuntimeException;
use OCP\AppFramework\Http;

/**
* @psalm-suppress UndefinedClass
* @psalm-suppress UndefinedDocblockClass
*/
final class WhiteboardController extends ApiController {
final class WhiteboardController extends ApiController
{
public function __construct(
$appName,
IRequest $request,
private JWTService $jwtService,
private AuthenticationService $authService,
private FileService $fileService,
private WhiteboardContentService $contentService,
Expand All @@ -40,10 +45,16 @@ public function __construct(
#[NoAdminRequired]
#[NoCSRFRequired]
#[PublicPage]
public function show(int $fileId): DataResponse {
public function show(int $fileId): DataResponse
{
try {
$userId = $this->authService->authenticateJWT($this->request);
$file = $this->fileService->getUserFileById($userId, $fileId);
$authHeader = $this->request->getHeader('Authorization');
if (sscanf($authHeader, 'Bearer %s', $jwt) !== 1) {
throw new RuntimeException('Unauthorized', Http::STATUS_UNAUTHORIZED);
}
$userId = $this->jwtService->getUserIdFromJWT($jwt);

Check failure on line 55 in lib/Controller/WhiteboardController.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis

PossiblyInvalidArgument

lib/Controller/WhiteboardController.php:55:50: PossiblyInvalidArgument: Argument 1 of OCA\Whiteboard\Service\JWTService::getUserIdFromJWT expects string, but possibly different type float|int|null|string provided (see https://psalm.dev/092)
$user = $this->authService->getWhiteboardUser($userId);
$file = $this->fileService->getFile($user, $fileId);
$data = $this->contentService->getContent($file);
return new DataResponse(['data' => $data]);
} catch (Exception $e) {
Expand All @@ -54,11 +65,14 @@ public function show(int $fileId): DataResponse {
#[NoAdminRequired]
#[NoCSRFRequired]
#[PublicPage]
public function update(int $fileId, array $data): DataResponse {
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);
$backendSharedToken = $this->request->getHeader('X-Whiteboard-Auth');
$this->authService->validateBackendSharedToken($backendSharedToken, $fileId);
$userId = $this->request->getHeader('X-Whiteboard-User');
$user = $this->authService->getWhiteboardUser($userId);
$file = $this->fileService->getFile($user, $fileId);
$this->contentService->updateContent($file, $data);
return new DataResponse(['status' => 'success']);
} catch (Exception $e) {
Expand Down
35 changes: 35 additions & 0 deletions lib/Listener/BeforeTemplateRenderedListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?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;

/** @template-implements IEventListener<BeforeTemplateRenderedEvent|Event> */
class BeforeTemplateRenderedListener implements IEventListener {

Check failure on line 19 in lib/Listener/BeforeTemplateRenderedListener.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis

UndefinedDocblockClass

lib/Listener/BeforeTemplateRenderedListener.php:19:7: UndefinedDocblockClass: Docblock-defined class, interface or enum named OCA\Files_Sharing\Event\BeforeTemplateRenderedEvent does not exist (see https://psalm.dev/200)

Check failure on line 19 in lib/Listener/BeforeTemplateRenderedListener.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis

InvalidTemplateParam

lib/Listener/BeforeTemplateRenderedListener.php:19:49: InvalidTemplateParam: Extended template param T expects type OCP\EventDispatcher\Event, type OCA\Files_Sharing\Event\BeforeTemplateRenderedEvent|OCP\EventDispatcher\Event given (see https://psalm.dev/183)
public function __construct(
private IInitialState $initialState,
) {
}

public function handle(Event $event): void {

Check failure on line 25 in lib/Listener/BeforeTemplateRenderedListener.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis

MoreSpecificImplementedParamType

lib/Listener/BeforeTemplateRenderedListener.php:25:31: MoreSpecificImplementedParamType: Argument 1 of OCA\Whiteboard\Listener\BeforeTemplateRenderedListener::handle has the more specific type 'OCP\EventDispatcher\Event', expecting 'OCA\Files_Sharing\Event\BeforeTemplateRenderedEvent|OCP\EventDispatcher\Event' as defined by OCP\EventDispatcher\IEventListener::handle (see https://psalm.dev/140)
if (!($event instanceof BeforeTemplateRenderedEvent)) {

Check failure on line 26 in lib/Listener/BeforeTemplateRenderedListener.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis

UndefinedClass

lib/Listener/BeforeTemplateRenderedListener.php:26:27: UndefinedClass: Class, interface or enum named OCA\Files_Sharing\Event\BeforeTemplateRenderedEvent does not exist (see https://psalm.dev/019)
return;
}

$this->initialState->provideInitialState(
'file_id',
$event->getShare()->getNodeId()
);
}
}
1 change: 1 addition & 0 deletions lib/Listener/RegisterTemplateCreatorListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public function handle(Event $event): void {
if (!($event instanceof RegisterTemplateCreatorEvent)) {
return;
}


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

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

namespace OCA\Whiteboard\Model;

use OCP\IUser;

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();
}
}
42 changes: 42 additions & 0 deletions lib/Model/PublicSharingUser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

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

namespace OCA\Whiteboard\Model;

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: 2022 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;
}
69 changes: 35 additions & 34 deletions lib/Service/AuthenticationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,13 @@
namespace OCA\Whiteboard\Service;

use Exception;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use OCA\Whiteboard\Consts\JWTConsts;
use OCA\Whiteboard\Model\AuthenticatedUser;
use OCA\Whiteboard\Model\PublicSharingUser;
use OCA\Whiteboard\Model\User;
use OCP\AppFramework\Http;
use OCP\IRequest;
use OCP\IUser;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\Share\IManager as ShareManager;
use RuntimeException;

/**
Expand All @@ -27,54 +26,49 @@ final class AuthenticationService {
public function __construct(
private ConfigService $configService,
private IUserManager $userManager,
private IUserSession $userSession
private IUserSession $userSession,
private ShareManager $shareManager
) {
}

/**
* @throws Exception
*/
public function authenticateJWT(IRequest $request): string {
$authHeader = $request->getHeader('Authorization');
if (!$authHeader || sscanf($authHeader, 'Bearer %s', $jwt) !== 1) {
throw new RuntimeException('Unauthorized', Http::STATUS_UNAUTHORIZED);
public function getAuthenticatedUser(?string $publicSharingToken = null): User {
if ($publicSharingToken) {
return $this->getPublicSharingUser($publicSharingToken);
}

if (!is_string($jwt)) {
throw new RuntimeException('JWT token must be a string', Http::STATUS_BAD_REQUEST);
if (!$this->userSession->isLoggedIn()) {
throw new RuntimeException('Unauthorized', Http::STATUS_UNAUTHORIZED);
}

try {
$key = $this->configService->getJwtSecretKey();

return JWT::decode($jwt, new Key($key, JWTConsts::JWT_ALGORITHM))->userid;
} catch (Exception) {
$user = $this->userSession->getUser();
if ($user === null) {
throw new RuntimeException('Unauthorized', Http::STATUS_UNAUTHORIZED);
}

return new AuthenticatedUser($user);
}

/**
* @throws Exception
*/
public function getAuthenticatedUser(): IUser {
if (!$this->userSession->isLoggedIn()) {
throw new RuntimeException('Unauthorized', Http::STATUS_UNAUTHORIZED);
}
private function getPublicSharingUser(string $publicSharingToken): User {
try {
$this->shareManager->getShareByToken($publicSharingToken);

$user = $this->userSession->getUser();
if ($user === null) {
throw new RuntimeException('Unauthorized', Http::STATUS_UNAUTHORIZED);
return new PublicSharingUser($publicSharingToken);
} catch (Exception $e) {
throw new RuntimeException('Invalid sharing token', Http::STATUS_UNAUTHORIZED);
}

return $user;
}

/**
* @throws Exception
*/
public function authenticateSharedToken(IRequest $request, int $fileId): void {
$whiteboardAuth = $request->getHeader('X-Whiteboard-Auth');
if (!$whiteboardAuth || !$this->verifySharedToken($whiteboardAuth, $fileId)) {
public function validateBackendSharedToken(string $backendSharedToken, int $fileId): void {
if (!$backendSharedToken || !$this->verifySharedToken($backendSharedToken, $fileId)) {
throw new RuntimeException('Unauthorized', Http::STATUS_UNAUTHORIZED);
}
}
Expand All @@ -96,15 +90,22 @@ private function verifySharedToken(string $token, int $fileId): bool {
/**
* @throws Exception
*/
public function getAndSetUser(IRequest $request): IUser {
$whiteboardUser = $request->getHeader('X-Whiteboard-User');
$user = $this->userManager->get($whiteboardUser);
public function getWhiteboardUser(string $userId): User {
if (str_starts_with($userId, 'shared_')) {
$parts = explode('_', $userId);
if (count($parts) < 3) {
throw new RuntimeException('Invalid user', Http::STATUS_BAD_REQUEST);
}
$sharingToken = $parts[1];
return new PublicSharingUser($sharingToken);
}

$user = $this->userManager->get($userId);
if (!$user) {
throw new RuntimeException('Invalid user', Http::STATUS_BAD_REQUEST);
}

$this->userSession->setVolatileActiveUser($user);

return $user;
return new AuthenticatedUser($user);
}
}
Loading

0 comments on commit bd1fcf6

Please sign in to comment.