From 8b2dc9cb71fc644b1d580940420336666c2818a3 Mon Sep 17 00:00:00 2001 From: sualko Date: Tue, 16 Jun 2020 16:54:50 +0200 Subject: [PATCH] feat: restrict room access to user and groups fix #25 --- lib/BigBlueButton/API.php | 48 ++------- lib/Controller/JoinController.php | 15 ++- lib/Db/RoomShareMapper.php | 13 +++ lib/Middleware/JoinMiddleware.php | 6 ++ lib/NoPermissionException.php | 7 ++ lib/NoPermissionResponse.php | 23 +++++ lib/Permission.php | 80 +++++++++++++++ lib/Service/RoomShareService.php | 18 ++-- tests/Unit/Controller/JoinControllerTest.php | 6 +- ts/Manager/EditRoom.tsx | 24 +++++ ts/Manager/EditRoomDialog.tsx | 101 +++++++++++-------- ts/Manager/RoomRow.tsx | 4 +- ts/Manager/ShareWith.scss | 11 ++ ts/Manager/ShareWith.tsx | 53 ++++++---- 14 files changed, 292 insertions(+), 117 deletions(-) create mode 100644 lib/NoPermissionException.php create mode 100644 lib/NoPermissionResponse.php create mode 100644 lib/Permission.php create mode 100644 ts/Manager/EditRoom.tsx diff --git a/lib/BigBlueButton/API.php b/lib/BigBlueButton/API.php index 23f1f95..db4baf4 100644 --- a/lib/BigBlueButton/API.php +++ b/lib/BigBlueButton/API.php @@ -11,6 +11,7 @@ use BigBlueButton\Parameters\DeleteRecordingsParameters; use BigBlueButton\Parameters\IsMeetingRunningParameters; use OCA\BigBlueButton\Db\Room; use OCA\BigBlueButton\Db\RoomShare; +use OCA\BigBlueButton\Permission; use OCA\BigBlueButton\Service\RoomShareService; use OCP\IConfig; use OCP\IURLGenerator; @@ -24,11 +25,8 @@ class API /** @var IURLGenerator */ private $urlGenerator; - /** @var IGroupManager */ - private $groupManager; - - /** @var RoomShareService */ - private $roomShareService; + /** @var Permission */ + private $permission; /** @var BigBlueButton */ private $server; @@ -36,13 +34,11 @@ class API public function __construct( IConfig $config, IURLGenerator $urlGenerator, - IGroupManager $groupManager, - RoomShareService $roomShareService + Permission $permission ) { $this->config = $config; $this->urlGenerator = $urlGenerator; - $this->groupManager = $groupManager; - $this->roomShareService = $roomShareService; + $this->permission = $permission; } private function getServer() @@ -64,7 +60,7 @@ class API */ public function createJoinUrl(Room $room, int $creationTime, string $displayname, string $uid = null) { - $password = $this->isModerator($room, $uid) ? $room->moderatorPassword : $room->attendeePassword; + $password = $this->permission->isModerator($room, $uid) ? $room->moderatorPassword : $room->attendeePassword; $joinMeetingParams = new JoinMeetingParameters($room->uid, $displayname, $password); @@ -81,38 +77,6 @@ class API return $this->getServer()->getJoinMeetingURL($joinMeetingParams); } - private function isModerator(Room $room, string $uid): bool - { - if ($uid === null) { - return false; - } - - if ($uid === $room->userId) { - return true; - } - - $shares = $this->roomShareService->findAll($room->id); - - /** @var RoomShare $share */ - foreach ($shares as $share) { - if (!$share->hasModeratorPermission()) { - continue; - } - - if ($share->getShareType() === RoomShare::SHARE_TYPE_USER) { - if ($share->getShareWith() === $uid) { - return true; - } - } elseif ($share->getShareType() === RoomShare::SHARE_TYPE_GROUP) { - if ($this->groupManager->isInGroup($uid, $share->getShareWith())) { - return true; - } - } - } - - return false; - } - /** * Create meeting room. * diff --git a/lib/Controller/JoinController.php b/lib/Controller/JoinController.php index 33e2e9f..2cafa92 100644 --- a/lib/Controller/JoinController.php +++ b/lib/Controller/JoinController.php @@ -4,7 +4,9 @@ namespace OCA\BigBlueButton\Controller; use OCA\BigBlueButton\BigBlueButton\API; use OCA\BigBlueButton\BigBlueButton\Presentation; use OCA\BigBlueButton\Db\Room; +use OCA\BigBlueButton\NoPermissionException; use OCA\BigBlueButton\NotFoundException; +use OCA\BigBlueButton\Permission; use OCP\AppFramework\Http\RedirectResponse; use OCP\IRequest; use OCP\ISession; @@ -38,6 +40,9 @@ class JoinController extends Controller /** @var API */ private $api; + /** @var Permission */ + private $permission; + public function __construct( string $appName, IRequest $request, @@ -46,7 +51,8 @@ class JoinController extends Controller IURLGenerator $urlGenerator, IUserSession $userSession, IConfig $config, - API $api + API $api, + Permission $permission ) { parent::__construct($appName, $request, $session); @@ -55,6 +61,7 @@ class JoinController extends Controller $this->userSession = $userSession; $this->config = $config; $this->api = $api; + $this->permission = $permission; } public function setToken(string $token) @@ -90,10 +97,14 @@ class JoinController extends Controller $displayname = $user->getDisplayName(); $userId = $user->getUID(); + if ($room->access == Room::ACCESS_INTERNAL_RESTRICTED && !$this->permission->isUser($room, $userId)) { + throw new NoPermissionException(); + } + if ($userId === $room->userId) { $presentation = new Presentation($u, $filename); } - } elseif ($room->access === Room::ACCESS_INTERNAL) { + } elseif ($room->access === Room::ACCESS_INTERNAL || $room->access == Room::ACCESS_INTERNAL_RESTRICTED) { return new RedirectResponse( $this->urlGenerator->linkToRoute('core.login.showLoginForm', [ 'redirect_url' => $this->urlGenerator->linkToRoute( diff --git a/lib/Db/RoomShareMapper.php b/lib/Db/RoomShareMapper.php index 37c32d4..d99fe63 100644 --- a/lib/Db/RoomShareMapper.php +++ b/lib/Db/RoomShareMapper.php @@ -30,6 +30,19 @@ class RoomShareMapper extends QBMapper return $this->findEntity($qb); } + public function findByRoomAndEntity(int $roomId, string $shareWith, int $shareType): RoomShare + { + /* @var $qb IQueryBuilder */ + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from('bbb_room_shares') + ->where($qb->expr()->eq('room_id', $qb->createNamedParameter($roomId, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($shareWith))) + ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter($shareType, IQueryBuilder::PARAM_INT))); + + return $this->findEntity($qb); + } + public function findAll(int $roomId): array { /* @var $qb IQueryBuilder */ diff --git a/lib/Middleware/JoinMiddleware.php b/lib/Middleware/JoinMiddleware.php index bc70eb2..cd05eb8 100644 --- a/lib/Middleware/JoinMiddleware.php +++ b/lib/Middleware/JoinMiddleware.php @@ -2,6 +2,8 @@ namespace OCA\BigBlueButton\Middleware; use OCA\BigBlueButton\Controller\JoinController; +use OCA\BigBlueButton\NoPermissionException; +use OCA\BigBlueButton\NoPermissionResponse; use OCA\BigBlueButton\NotFoundException; use OCP\AppFramework\Middleware; use OCP\AppFramework\Http\NotFoundResponse; @@ -47,6 +49,10 @@ class JoinMiddleware extends Middleware return new NotFoundResponse(); } + if ($exception instanceof NoPermissionException) { + return new NoPermissionResponse(); + } + throw $exception; } } diff --git a/lib/NoPermissionException.php b/lib/NoPermissionException.php new file mode 100644 index 0000000..c95d375 --- /dev/null +++ b/lib/NoPermissionException.php @@ -0,0 +1,7 @@ +setContentSecurityPolicy(new ContentSecurityPolicy()); + $this->setStatus(404); + } + + public function render() + { + $template = new Template('core', '403', 'guest'); + return $template->fetchPage(); + } +} diff --git a/lib/Permission.php b/lib/Permission.php new file mode 100644 index 0000000..28aeb7d --- /dev/null +++ b/lib/Permission.php @@ -0,0 +1,80 @@ +groupManager = $groupManager; + $this->roomShareService = $roomShareService; + } + + public function isUser(Room $room, string $uid) + { + return $this->hasPermission($room, $uid, function (RoomShare $share) { + return $share->hasUserPermission(); + }); + } + + public function isModerator(Room $room, string $uid) + { + return $this->hasPermission($room, $uid, function (RoomShare $share) { + return $share->hasModeratorPermission(); + }); + } + + public function isAdmin(Room $room, string $uid) + { + return $this->hasPermission($room, $uid, function (RoomShare $share) { + return $share->hasAdminPermission(); + }); + } + + private function hasPermission(Room $room, string $uid, Closure $hasPermission): bool + { + if ($uid === null) { + return false; + } + + if ($uid === $room->userId) { + return true; + } + + $shares = $this->roomShareService->findAll($room->id); + + /** @var RoomShare $share */ + foreach ($shares as $share) { + if (!$hasPermission($share)) { + continue; + } + + if ($share->getShareType() === RoomShare::SHARE_TYPE_USER) { + if ($share->getShareWith() === $uid) { + return true; + } + } elseif ($share->getShareType() === RoomShare::SHARE_TYPE_GROUP) { + if ($this->groupManager->isInGroup($uid, $share->getShareWith())) { + return true; + } + } + } + + return false; + } +} diff --git a/lib/Service/RoomShareService.php b/lib/Service/RoomShareService.php index 5429bdd..9ba4f01 100644 --- a/lib/Service/RoomShareService.php +++ b/lib/Service/RoomShareService.php @@ -46,14 +46,20 @@ class RoomShareService public function create(int $roomId, int $shareType, string $shareWith, int $permission): RoomShare { - $roomShare = new RoomShare(); + try { + $roomShare = $this->mapper->findByRoomAndEntity($roomId, $shareWith, $shareType); - $roomShare->setRoomId($roomId); - $roomShare->setShareType($shareType); - $roomShare->setShareWith($shareWith); - $roomShare->setPermission($permission); + return $this->update($roomShare->getId(), $roomId, $shareType, $shareWith, $permission); + } catch (DoesNotExistException $e) { + $roomShare = new RoomShare(); - return $this->mapper->insert($roomShare); + $roomShare->setRoomId($roomId); + $roomShare->setShareType($shareType); + $roomShare->setShareWith($shareWith); + $roomShare->setPermission($permission); + + return $this->mapper->insert($roomShare); + } } public function update(int $id, int $roomId, int $shareType, string $shareWith, int $permission): RoomShare diff --git a/tests/Unit/Controller/JoinControllerTest.php b/tests/Unit/Controller/JoinControllerTest.php index 8cf27d0..a29309e 100644 --- a/tests/Unit/Controller/JoinControllerTest.php +++ b/tests/Unit/Controller/JoinControllerTest.php @@ -15,6 +15,7 @@ use OCA\BigBlueButton\Controller\JoinController; use OCA\BigBlueButton\BigBlueButton\API; use OCA\BigBlueButton\NotFoundException; use OCA\BigBlueButton\Db\Room; +use OCA\BigBlueButton\Permission; class JoinControllerTest extends TestCase { @@ -25,6 +26,7 @@ class JoinControllerTest extends TestCase private $urlGenerator; private $controller; private $api; + private $permission; private $room; public function setUp(): void @@ -38,6 +40,7 @@ class JoinControllerTest extends TestCase $this->config = $this->createMock(IConfig::class); $this->urlGenerator = $this->createMock(IURLGenerator::class); $this->api = $this->createMock(API::class); + $this->permission = $this->createMock(Permission::class); $this->controller = new JoinController( 'bbb', @@ -47,7 +50,8 @@ class JoinControllerTest extends TestCase $this->urlGenerator, $this->userSession, $this->config, - $this->api + $this->api, + $this->permission ); $this->room = new Room(); diff --git a/ts/Manager/EditRoom.tsx b/ts/Manager/EditRoom.tsx new file mode 100644 index 0000000..aa4548f --- /dev/null +++ b/ts/Manager/EditRoom.tsx @@ -0,0 +1,24 @@ +import React, { useState } from 'react'; +import { Room } from './Api'; +import EditRoomDialog from './EditRoomDialog'; + +type Props = { + room: Room; + updateProperty: (key: string, value: string | boolean | number) => Promise; +} + +const EditRoom: React.FC = ({ room, updateProperty }) => { + const [open, setOpen] = useState(false); + + return ( + <> + { ev.preventDefault(), setOpen(true); }} + title={t('bbb', 'Edit')} /> + + + + ); +}; + +export default EditRoom; \ No newline at end of file diff --git a/ts/Manager/EditRoomDialog.tsx b/ts/Manager/EditRoomDialog.tsx index d99b006..478a7fb 100644 --- a/ts/Manager/EditRoomDialog.tsx +++ b/ts/Manager/EditRoomDialog.tsx @@ -1,5 +1,5 @@ -import React, { useState } from 'react'; -import { Access, Room } from './Api'; +import React, { useState, useEffect } from 'react'; +import { Access, Room, Permission, RoomShare, api } from './Api'; import Dialog from './Dialog'; import ShareWith from './ShareWith'; import { SubmitInput } from './SubmitInput'; @@ -13,12 +13,29 @@ const descriptions: { [key: string]: string } = { }; type Props = { - room: Room; - updateProperty: (key: string, value: string | boolean | number) => Promise; + room: Room; + updateProperty: (key: string, value: string | boolean | number) => Promise; + open: boolean; + setOpen: (open: boolean) => void; } -const EditRoomDialog: React.FC = ({ room, updateProperty }) => { - const [open, setOpen] = useState(false); +const EditRoomDialog: React.FC = ({ room, updateProperty, open, setOpen }) => { + const [shares, setShares] = useState(); + + useEffect(() => { + if (!open) { + return; + } + + api.getRoomShares(room.id).then(roomShares => { + console.log(room.name, roomShares); + setShares(roomShares); + }).catch(err => { + console.warn('Could not load room shares.', err); + + setShares([]); + }); + }, [room.id, open]); function inputElement(label: string, field: string, type: 'text' | 'number' = 'text') { return ( @@ -33,7 +50,7 @@ const EditRoomDialog: React.FC = ({ room, updateProperty }) => { ); } - function selectElement(label: string, field: string, value: string, options: {[key: string]: string}, onChange: (value: string) => void) { + function selectElement(label: string, field: string, value: string, options: { [key: string]: string }, onChange: (value: string) => void) { return (
{ ev.preventDefault(), setOpen(true); }} - title={t('bbb', 'Edit')} /> + setOpen(false)} title={t('bbb', 'Edit "{room}"', { room: room.name })}> + {inputElement(t('bbb', 'Name'), 'name')} + {inputElement(t('bbb', 'Welcome'), 'welcome')} + {inputElement(t('bbb', 'Participant limit'), 'maxParticipants', 'number')} - setOpen(false)} title={t('bbb', 'Edit "{room}"', { room: room.name })}> - {inputElement(t('bbb', 'Name'), 'name')} - {inputElement(t('bbb', 'Welcome'), 'welcome')} - {inputElement(t('bbb', 'Participant limit'), 'maxParticipants', 'number')} + {selectElement(t('bbb', 'Access'), 'access', room.access, { + [Access.Public]: t('bbb', 'Public'), + [Access.Password]: t('bbb', 'Internal + Password protection for guests'), + [Access.WaitingRoom]: t('bbb', 'Internal + Waiting room for guests'), + [Access.Internal]: t('bbb', 'Internal'), + [Access.InternalRestricted]: t('bbb', 'Internal restricted'), + }, (value) => { + console.log('access', value); + updateProperty('access', value); + })} - {selectElement(t('bbb', 'Access'), 'access', room.access, { - [Access.Public]: t('bbb', 'Public'), - [Access.Password]: t('bbb', 'Internal + Password protection for guests'), - [Access.WaitingRoom]: t('bbb', 'Internal + Waiting room for guests'), - [Access.Internal]: t('bbb', 'Internal'), - // [Access.InternalRestricted]: t('bbb', 'Restricted'), - }, (value) => { - console.log('access', value); - updateProperty('access', value); - })} + {room.access === Access.InternalRestricted &&
+ +
} -
- +
+ - -
+ +
-

{t('bbb', 'Miscellaneous')}

+

{t('bbb', 'Miscellaneous')}

+
-
- updateProperty('record', event.target.checked)} /> - -
- {descriptions.recording} + updateProperty('record', event.target.checked)} /> +
-
- + {descriptions.recording} +
+ ); }; diff --git a/ts/Manager/RoomRow.tsx b/ts/Manager/RoomRow.tsx index c3a78f6..da76d23 100644 --- a/ts/Manager/RoomRow.tsx +++ b/ts/Manager/RoomRow.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; import { CopyToClipboard } from 'react-copy-to-clipboard'; import { api, Recording, Room } from './Api'; -import EditRoomDialog from './EditRoomDialog'; +import EditRoom from './EditRoom'; import RecordingRow from './RecordingRow'; import EditableValue from './EditableValue'; @@ -184,7 +184,7 @@ const RoomRow: React.FC = (props) => { - + void; } -const SearchInput: React.FC = ({ room }) => { +const ShareWith: React.FC = ({ room, permission, shares: allShares, setShares }) => { const [search, setSearch] = useState(''); const [hasFocus, setFocus] = useState(false); const [recommendations, setRecommendations] = useState(); const [searchResults, setSearchResults] = useState(); - const [shares, setShares] = useState(); - const userShares = shares ? shares.filter(share => share.shareType === ShareType.User).map(share => share.shareWith) : []; - const groupShares = shares ? shares.filter(share => share.shareType === ShareType.Group).map(share => share.shareWith) : []; + const shares = (allShares && permission === Permission.Moderator) ? + allShares.filter(share => share.permission !== Permission.User) : allShares; - useEffect(() => { - api.getRoomShares(room.id).then(roomShares => { - setShares(roomShares); - }).catch(err => { - console.warn('Could not load room shares.', err); - - setShares([]); - }); - }, [room.id]); + const sharedUserIds = shares ? shares.filter(share => share.shareType === ShareType.User).map(share => share.shareWith) : []; + const sharedGroupIds = shares ? shares.filter(share => share.shareType === ShareType.Group).map(share => share.shareWith) : []; useEffect(() => { api.searchShareWith(search).then(result => { @@ -37,25 +32,40 @@ const SearchInput: React.FC = ({ room }) => { }, []); async function addRoomShare(shareWith: string, shareType: number, displayName: string) { - const roomShare = await api.createRoomShare(room.id, shareType, shareWith, Permission.Moderator); + const roomShare = await api.createRoomShare(room.id, shareType, shareWith, permission); roomShare.shareWithDisplayName = displayName; - setShares([...(shares || []), roomShare]); + console.log('addRoomShare', allShares, roomShare); + + const newShares = allShares ? [...allShares] : []; + const index = newShares.findIndex(share => share.id === roomShare.id); + + if (index > -1) { + newShares[index] = roomShare; + } else { + newShares.push(roomShare); + } + + console.log('newroomshares', newShares); + + setShares(newShares); } async function deleteRoomShare(id: number) { + console.log('deleteRoomShare', id); + await api.deleteRoomShare(id); - setShares(shares?.filter(share => share.id !== id)); + setShares((allShares ? [...allShares] : []).filter(share => share.id !== id)); } function renderSearchResults(options: ShareWith) { return (