feat: restrict room access to user and groups

fix #25
pull/63/head
sualko 2020-06-16 16:54:50 +02:00
parent bec1b4dce7
commit 8b2dc9cb71
14 changed files with 292 additions and 117 deletions

View File

@ -11,6 +11,7 @@ use BigBlueButton\Parameters\DeleteRecordingsParameters;
use BigBlueButton\Parameters\IsMeetingRunningParameters; use BigBlueButton\Parameters\IsMeetingRunningParameters;
use OCA\BigBlueButton\Db\Room; use OCA\BigBlueButton\Db\Room;
use OCA\BigBlueButton\Db\RoomShare; use OCA\BigBlueButton\Db\RoomShare;
use OCA\BigBlueButton\Permission;
use OCA\BigBlueButton\Service\RoomShareService; use OCA\BigBlueButton\Service\RoomShareService;
use OCP\IConfig; use OCP\IConfig;
use OCP\IURLGenerator; use OCP\IURLGenerator;
@ -24,11 +25,8 @@ class API
/** @var IURLGenerator */ /** @var IURLGenerator */
private $urlGenerator; private $urlGenerator;
/** @var IGroupManager */ /** @var Permission */
private $groupManager; private $permission;
/** @var RoomShareService */
private $roomShareService;
/** @var BigBlueButton */ /** @var BigBlueButton */
private $server; private $server;
@ -36,13 +34,11 @@ class API
public function __construct( public function __construct(
IConfig $config, IConfig $config,
IURLGenerator $urlGenerator, IURLGenerator $urlGenerator,
IGroupManager $groupManager, Permission $permission
RoomShareService $roomShareService
) { ) {
$this->config = $config; $this->config = $config;
$this->urlGenerator = $urlGenerator; $this->urlGenerator = $urlGenerator;
$this->groupManager = $groupManager; $this->permission = $permission;
$this->roomShareService = $roomShareService;
} }
private function getServer() private function getServer()
@ -64,7 +60,7 @@ class API
*/ */
public function createJoinUrl(Room $room, int $creationTime, string $displayname, string $uid = null) 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); $joinMeetingParams = new JoinMeetingParameters($room->uid, $displayname, $password);
@ -81,38 +77,6 @@ class API
return $this->getServer()->getJoinMeetingURL($joinMeetingParams); 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. * Create meeting room.
* *

View File

@ -4,7 +4,9 @@ namespace OCA\BigBlueButton\Controller;
use OCA\BigBlueButton\BigBlueButton\API; use OCA\BigBlueButton\BigBlueButton\API;
use OCA\BigBlueButton\BigBlueButton\Presentation; use OCA\BigBlueButton\BigBlueButton\Presentation;
use OCA\BigBlueButton\Db\Room; use OCA\BigBlueButton\Db\Room;
use OCA\BigBlueButton\NoPermissionException;
use OCA\BigBlueButton\NotFoundException; use OCA\BigBlueButton\NotFoundException;
use OCA\BigBlueButton\Permission;
use OCP\AppFramework\Http\RedirectResponse; use OCP\AppFramework\Http\RedirectResponse;
use OCP\IRequest; use OCP\IRequest;
use OCP\ISession; use OCP\ISession;
@ -38,6 +40,9 @@ class JoinController extends Controller
/** @var API */ /** @var API */
private $api; private $api;
/** @var Permission */
private $permission;
public function __construct( public function __construct(
string $appName, string $appName,
IRequest $request, IRequest $request,
@ -46,7 +51,8 @@ class JoinController extends Controller
IURLGenerator $urlGenerator, IURLGenerator $urlGenerator,
IUserSession $userSession, IUserSession $userSession,
IConfig $config, IConfig $config,
API $api API $api,
Permission $permission
) { ) {
parent::__construct($appName, $request, $session); parent::__construct($appName, $request, $session);
@ -55,6 +61,7 @@ class JoinController extends Controller
$this->userSession = $userSession; $this->userSession = $userSession;
$this->config = $config; $this->config = $config;
$this->api = $api; $this->api = $api;
$this->permission = $permission;
} }
public function setToken(string $token) public function setToken(string $token)
@ -90,10 +97,14 @@ class JoinController extends Controller
$displayname = $user->getDisplayName(); $displayname = $user->getDisplayName();
$userId = $user->getUID(); $userId = $user->getUID();
if ($room->access == Room::ACCESS_INTERNAL_RESTRICTED && !$this->permission->isUser($room, $userId)) {
throw new NoPermissionException();
}
if ($userId === $room->userId) { if ($userId === $room->userId) {
$presentation = new Presentation($u, $filename); $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( return new RedirectResponse(
$this->urlGenerator->linkToRoute('core.login.showLoginForm', [ $this->urlGenerator->linkToRoute('core.login.showLoginForm', [
'redirect_url' => $this->urlGenerator->linkToRoute( 'redirect_url' => $this->urlGenerator->linkToRoute(

View File

@ -30,6 +30,19 @@ class RoomShareMapper extends QBMapper
return $this->findEntity($qb); 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 public function findAll(int $roomId): array
{ {
/* @var $qb IQueryBuilder */ /* @var $qb IQueryBuilder */

View File

@ -2,6 +2,8 @@
namespace OCA\BigBlueButton\Middleware; namespace OCA\BigBlueButton\Middleware;
use OCA\BigBlueButton\Controller\JoinController; use OCA\BigBlueButton\Controller\JoinController;
use OCA\BigBlueButton\NoPermissionException;
use OCA\BigBlueButton\NoPermissionResponse;
use OCA\BigBlueButton\NotFoundException; use OCA\BigBlueButton\NotFoundException;
use OCP\AppFramework\Middleware; use OCP\AppFramework\Middleware;
use OCP\AppFramework\Http\NotFoundResponse; use OCP\AppFramework\Http\NotFoundResponse;
@ -47,6 +49,10 @@ class JoinMiddleware extends Middleware
return new NotFoundResponse(); return new NotFoundResponse();
} }
if ($exception instanceof NoPermissionException) {
return new NoPermissionResponse();
}
throw $exception; throw $exception;
} }
} }

View File

@ -0,0 +1,7 @@
<?php
namespace OCA\BigBlueButton;
class NoPermissionException extends \Exception
{
}

View File

@ -0,0 +1,23 @@
<?php
namespace OCA\BigBlueButton;
use OCP\Template;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\ContentSecurityPolicy;
class NoPermissionResponse extends Response
{
public function __construct()
{
parent::__construct();
$this->setContentSecurityPolicy(new ContentSecurityPolicy());
$this->setStatus(404);
}
public function render()
{
$template = new Template('core', '403', 'guest');
return $template->fetchPage();
}
}

80
lib/Permission.php Normal file
View File

@ -0,0 +1,80 @@
<?php
namespace OCA\BigBlueButton;
use Closure;
use OCA\BigBlueButton\Service\RoomShareService;
use OCA\BigBlueButton\Db\Room;
use OCA\BigBlueButton\Db\RoomShare;
use OCP\IGroupManager;
class Permission
{
/** @var IGroupManager */
private $groupManager;
/** @var RoomShareService */
private $roomShareService;
public function __construct(
IGroupManager $groupManager,
RoomShareService $roomShareService
) {
$this->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;
}
}

View File

@ -46,6 +46,11 @@ class RoomShareService
public function create(int $roomId, int $shareType, string $shareWith, int $permission): RoomShare public function create(int $roomId, int $shareType, string $shareWith, int $permission): RoomShare
{ {
try {
$roomShare = $this->mapper->findByRoomAndEntity($roomId, $shareWith, $shareType);
return $this->update($roomShare->getId(), $roomId, $shareType, $shareWith, $permission);
} catch (DoesNotExistException $e) {
$roomShare = new RoomShare(); $roomShare = new RoomShare();
$roomShare->setRoomId($roomId); $roomShare->setRoomId($roomId);
@ -55,6 +60,7 @@ class RoomShareService
return $this->mapper->insert($roomShare); return $this->mapper->insert($roomShare);
} }
}
public function update(int $id, int $roomId, int $shareType, string $shareWith, int $permission): RoomShare public function update(int $id, int $roomId, int $shareType, string $shareWith, int $permission): RoomShare
{ {

View File

@ -15,6 +15,7 @@ use OCA\BigBlueButton\Controller\JoinController;
use OCA\BigBlueButton\BigBlueButton\API; use OCA\BigBlueButton\BigBlueButton\API;
use OCA\BigBlueButton\NotFoundException; use OCA\BigBlueButton\NotFoundException;
use OCA\BigBlueButton\Db\Room; use OCA\BigBlueButton\Db\Room;
use OCA\BigBlueButton\Permission;
class JoinControllerTest extends TestCase class JoinControllerTest extends TestCase
{ {
@ -25,6 +26,7 @@ class JoinControllerTest extends TestCase
private $urlGenerator; private $urlGenerator;
private $controller; private $controller;
private $api; private $api;
private $permission;
private $room; private $room;
public function setUp(): void public function setUp(): void
@ -38,6 +40,7 @@ class JoinControllerTest extends TestCase
$this->config = $this->createMock(IConfig::class); $this->config = $this->createMock(IConfig::class);
$this->urlGenerator = $this->createMock(IURLGenerator::class); $this->urlGenerator = $this->createMock(IURLGenerator::class);
$this->api = $this->createMock(API::class); $this->api = $this->createMock(API::class);
$this->permission = $this->createMock(Permission::class);
$this->controller = new JoinController( $this->controller = new JoinController(
'bbb', 'bbb',
@ -47,7 +50,8 @@ class JoinControllerTest extends TestCase
$this->urlGenerator, $this->urlGenerator,
$this->userSession, $this->userSession,
$this->config, $this->config,
$this->api $this->api,
$this->permission
); );
$this->room = new Room(); $this->room = new Room();

24
ts/Manager/EditRoom.tsx Normal file
View File

@ -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<void>;
}
const EditRoom: React.FC<Props> = ({ room, updateProperty }) => {
const [open, setOpen] = useState<boolean>(false);
return (
<>
<a className="icon icon-edit icon-visible"
onClick={ev => { ev.preventDefault(), setOpen(true); }}
title={t('bbb', 'Edit')} />
<EditRoomDialog room={room} updateProperty={updateProperty} open={open} setOpen={setOpen} />
</>
);
};
export default EditRoom;

View File

@ -1,5 +1,5 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { Access, Room } from './Api'; import { Access, Room, Permission, RoomShare, api } from './Api';
import Dialog from './Dialog'; import Dialog from './Dialog';
import ShareWith from './ShareWith'; import ShareWith from './ShareWith';
import { SubmitInput } from './SubmitInput'; import { SubmitInput } from './SubmitInput';
@ -15,10 +15,27 @@ const descriptions: { [key: string]: string } = {
type Props = { type Props = {
room: Room; room: Room;
updateProperty: (key: string, value: string | boolean | number) => Promise<void>; updateProperty: (key: string, value: string | boolean | number) => Promise<void>;
open: boolean;
setOpen: (open: boolean) => void;
} }
const EditRoomDialog: React.FC<Props> = ({ room, updateProperty }) => { const EditRoomDialog: React.FC<Props> = ({ room, updateProperty, open, setOpen }) => {
const [open, setOpen] = useState<boolean>(false); const [shares, setShares] = useState<RoomShare[]>();
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') { function inputElement(label: string, field: string, type: 'text' | 'number' = 'text') {
return ( return (
@ -33,7 +50,7 @@ const EditRoomDialog: React.FC<Props> = ({ 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 ( return (
<div className="bbb-form-element"> <div className="bbb-form-element">
<label htmlFor={`bbb-${field}`}> <label htmlFor={`bbb-${field}`}>
@ -54,11 +71,6 @@ const EditRoomDialog: React.FC<Props> = ({ room, updateProperty }) => {
} }
return ( return (
<>
<a className="icon icon-edit icon-visible"
onClick={ev => { ev.preventDefault(), setOpen(true); }}
title={t('bbb', 'Edit')} />
<Dialog open={open} onClose={() => setOpen(false)} title={t('bbb', 'Edit "{room}"', { room: room.name })}> <Dialog open={open} onClose={() => setOpen(false)} title={t('bbb', 'Edit "{room}"', { room: room.name })}>
{inputElement(t('bbb', 'Name'), 'name')} {inputElement(t('bbb', 'Name'), 'name')}
{inputElement(t('bbb', 'Welcome'), 'welcome')} {inputElement(t('bbb', 'Welcome'), 'welcome')}
@ -69,18 +81,22 @@ const EditRoomDialog: React.FC<Props> = ({ room, updateProperty }) => {
[Access.Password]: t('bbb', 'Internal + Password protection for guests'), [Access.Password]: t('bbb', 'Internal + Password protection for guests'),
[Access.WaitingRoom]: t('bbb', 'Internal + Waiting room for guests'), [Access.WaitingRoom]: t('bbb', 'Internal + Waiting room for guests'),
[Access.Internal]: t('bbb', 'Internal'), [Access.Internal]: t('bbb', 'Internal'),
// [Access.InternalRestricted]: t('bbb', 'Restricted'), [Access.InternalRestricted]: t('bbb', 'Internal restricted'),
}, (value) => { }, (value) => {
console.log('access', value); console.log('access', value);
updateProperty('access', value); updateProperty('access', value);
})} })}
{room.access === Access.InternalRestricted && <div className="bbb-form-element bbb-form-shareWith">
<ShareWith permission={Permission.User} room={room} shares={shares} setShares={setShares} />
</div>}
<div className="bbb-form-element"> <div className="bbb-form-element">
<label htmlFor={'bbb-moderator'}> <label htmlFor={'bbb-moderator'}>
<h3>Moderator</h3> <h3>Moderator</h3>
</label> </label>
<ShareWith room={room} /> <ShareWith permission={Permission.Moderator} room={room} shares={shares} setShares={setShares} />
</div> </div>
<h3>{t('bbb', 'Miscellaneous')}</h3> <h3>{t('bbb', 'Miscellaneous')}</h3>
@ -96,7 +112,6 @@ const EditRoomDialog: React.FC<Props> = ({ room, updateProperty }) => {
<em>{descriptions.recording}</em> <em>{descriptions.recording}</em>
</div> </div>
</Dialog> </Dialog>
</>
); );
}; };

View File

@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { CopyToClipboard } from 'react-copy-to-clipboard'; import { CopyToClipboard } from 'react-copy-to-clipboard';
import { api, Recording, Room } from './Api'; import { api, Recording, Room } from './Api';
import EditRoomDialog from './EditRoomDialog'; import EditRoom from './EditRoom';
import RecordingRow from './RecordingRow'; import RecordingRow from './RecordingRow';
import EditableValue from './EditableValue'; import EditableValue from './EditableValue';
@ -184,7 +184,7 @@ const RoomRow: React.FC<Props> = (props) => {
</td> </td>
<td><RecordingsNumber recordings={recordings} showRecordings={showRecordings} setShowRecordings={setShowRecordings} /></td> <td><RecordingsNumber recordings={recordings} showRecordings={showRecordings} setShowRecordings={setShowRecordings} /></td>
<td className="edit icon-col"> <td className="edit icon-col">
<EditRoomDialog room={props.room} updateProperty={updateRoom} /> <EditRoom room={props.room} updateProperty={updateRoom} />
</td> </td>
<td className="remove icon-col"> <td className="remove icon-col">
<a className="icon icon-delete icon-visible" <a className="icon icon-delete icon-visible"

View File

@ -14,6 +14,13 @@
height: 32px; height: 32px;
width: 32px; width: 32px;
overflow: hidden; overflow: hidden;
.icon-group-white {
display: block;
height: 100%;
width: 100%;
background-color: #a9a9a9;
}
} }
.icon { .icon {
@ -51,3 +58,7 @@
} }
} }
} }
.bbb-form-shareWith {
margin-top: -1.5em;
}

View File

@ -4,27 +4,22 @@ import './ShareWith.scss';
type Props = { type Props = {
room: Room; room: Room;
permission: Permission.User | Permission.Moderator;
shares: RoomShare[] | undefined;
setShares: (shares: RoomShare[]) => void;
} }
const SearchInput: React.FC<Props> = ({ room }) => { const ShareWith: React.FC<Props> = ({ room, permission, shares: allShares, setShares }) => {
const [search, setSearch] = useState<string>(''); const [search, setSearch] = useState<string>('');
const [hasFocus, setFocus] = useState<boolean>(false); const [hasFocus, setFocus] = useState<boolean>(false);
const [recommendations, setRecommendations] = useState<ShareWith>(); const [recommendations, setRecommendations] = useState<ShareWith>();
const [searchResults, setSearchResults] = useState<ShareWith>(); const [searchResults, setSearchResults] = useState<ShareWith>();
const [shares, setShares] = useState<RoomShare[]>();
const userShares = shares ? shares.filter(share => share.shareType === ShareType.User).map(share => share.shareWith) : []; const shares = (allShares && permission === Permission.Moderator) ?
const groupShares = shares ? shares.filter(share => share.shareType === ShareType.Group).map(share => share.shareWith) : []; allShares.filter(share => share.permission !== Permission.User) : allShares;
useEffect(() => { const sharedUserIds = shares ? shares.filter(share => share.shareType === ShareType.User).map(share => share.shareWith) : [];
api.getRoomShares(room.id).then(roomShares => { const sharedGroupIds = shares ? shares.filter(share => share.shareType === ShareType.Group).map(share => share.shareWith) : [];
setShares(roomShares);
}).catch(err => {
console.warn('Could not load room shares.', err);
setShares([]);
});
}, [room.id]);
useEffect(() => { useEffect(() => {
api.searchShareWith(search).then(result => { api.searchShareWith(search).then(result => {
@ -37,25 +32,40 @@ const SearchInput: React.FC<Props> = ({ room }) => {
}, []); }, []);
async function addRoomShare(shareWith: string, shareType: number, displayName: string) { 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; 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) { async function deleteRoomShare(id: number) {
console.log('deleteRoomShare', id);
await api.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) { function renderSearchResults(options: ShareWith) {
return ( return (
<ul className="bbb-selection"> <ul className="bbb-selection">
{[ {[
...options.users.filter(user => !userShares.includes(user.value.shareWith)), ...options.users.filter(user => !sharedUserIds.includes(user.value.shareWith)),
...options.groups.filter(group => !groupShares.includes(group.value.shareWith)), ...options.groups.filter(group => !sharedGroupIds.includes(group.value.shareWith)),
].map(option => { ].map(option => {
return (<li key={option.value.shareWith} onClick={() => addRoomShare(option.value.shareWith, option.value.shareType, option.label)}> return (<li key={option.value.shareWith} onClick={() => addRoomShare(option.value.shareWith, option.value.shareType, option.label)}>
{option.label}{option.value.shareType === ShareType.Group ? ` (${t('bbb', 'Group')})` : ''} {option.label}{option.value.shareType === ShareType.Group ? ` (${t('bbb', 'Group')})` : ''}
@ -90,9 +100,10 @@ const SearchInput: React.FC<Props> = ({ room }) => {
<li key={share.id} className="bbb-shareWith__item"> <li key={share.id} className="bbb-shareWith__item">
<div className="avatardiv"> <div className="avatardiv">
{avatarUrl && <img src={avatarUrl} alt={`Avatar from ${displayName}`} />} {avatarUrl && <img src={avatarUrl} alt={`Avatar from ${displayName}`} />}
{share.shareType === ShareType.Group && <span className="icon-group-white"></span>}
</div> </div>
<div className="bbb-shareWith__item__label"> <div className="bbb-shareWith__item__label">
<h5>{displayName}{share.shareType === ShareType.Group ? ` (${t('bbb', 'Group')})` : ''}</h5> <h5>{displayName}{(share.permission === Permission.Moderator && permission === Permission.User) ? ` (${t('bbb', 'moderator')})` : ''}</h5>
</div> </div>
{share.id > -1 && <div className="bbb-shareWith__item__action"> {share.id > -1 && <div className="bbb-shareWith__item__action">
<a className="icon icon-delete icon-visible" <a className="icon icon-delete icon-visible"
@ -126,4 +137,4 @@ const SearchInput: React.FC<Props> = ({ room }) => {
); );
}; };
export default SearchInput; export default ShareWith;