From a49258e840b1813bf8aa21736d90db3241722381 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Fri, 8 May 2020 16:19:55 +0200 Subject: [PATCH] New option for handling permissions in rooms. Set allowWhenRoleMissing to permit actions before a peer with that permission joins. Ref #303 --- app/src/RoomClient.js | 10 +- app/src/actions/roomActions.js | 12 +- .../AccessControl/LockDialog/ListLobbyPeer.js | 28 ++- .../AccessControl/LockDialog/LockDialog.js | 26 +- app/src/components/Containers/Me.js | 37 +-- app/src/components/Controls/TopBar.js | 56 +++-- .../MeetingDrawer/Chat/ChatInput.js | 28 ++- .../MeetingDrawer/Chat/ChatModerator.js | 26 +- .../MeetingDrawer/FileSharing/FileSharing.js | 28 ++- .../FileSharing/FileSharingModerator.js | 26 +- .../ParticipantList/ParticipantList.js | 33 +-- app/src/components/Selectors.js | 53 ++++ app/src/permissions.js | 20 ++ app/src/reducers/room.js | 26 +- server/access.js | 11 + server/config/config.example.js | 52 ++-- server/lib/Room.js | 233 ++++++++---------- server/permissions.js | 20 ++ 18 files changed, 437 insertions(+), 288 deletions(-) create mode 100644 app/src/permissions.js create mode 100644 server/access.js create mode 100644 server/permissions.js diff --git a/app/src/RoomClient.js b/app/src/RoomClient.js index cffdd2a..2b2b387 100644 --- a/app/src/RoomClient.js +++ b/app/src/RoomClient.js @@ -2847,8 +2847,8 @@ export default class RoomClient roles, peers, tracker, - permissionsFromRoles, - userRoles, + roomPermissions, + allowWhenRoleMissing, chatHistory, fileHistory, lastNHistory, @@ -2874,8 +2874,10 @@ export default class RoomClient store.dispatch(meActions.loggedIn(authenticated)); - store.dispatch(roomActions.setUserRoles(userRoles)); - store.dispatch(roomActions.setPermissionsFromRoles(permissionsFromRoles)); + store.dispatch(roomActions.setRoomPermissions(roomPermissions)); + + if (allowWhenRoleMissing) + store.dispatch(roomActions.setAllowWhenRoleMissing(allowWhenRoleMissing)); const myRoles = store.getState().me.roles; diff --git a/app/src/actions/roomActions.js b/app/src/actions/roomActions.js index 7c44835..cbfde37 100644 --- a/app/src/actions/roomActions.js +++ b/app/src/actions/roomActions.js @@ -177,14 +177,14 @@ export const setClearFileSharingInProgress = (flag) => payload : { flag } }); -export const setUserRoles = (userRoles) => +export const setRoomPermissions = (roomPermissions) => ({ - type : 'SET_USER_ROLES', - payload : { userRoles } + type : 'SET_ROOM_PERMISSIONS', + payload : { roomPermissions } }); -export const setPermissionsFromRoles = (permissionsFromRoles) => +export const setAllowWhenRoleMissing = (allowWhenRoleMissing) => ({ - type : 'SET_PERMISSIONS_FROM_ROLES', - payload : { permissionsFromRoles } + type : 'SET_ALLOW_WHEN_ROLE_MISSING', + payload : { allowWhenRoleMissing } }); diff --git a/app/src/components/AccessControl/LockDialog/ListLobbyPeer.js b/app/src/components/AccessControl/LockDialog/ListLobbyPeer.js index 9e73e82..050994d 100644 --- a/app/src/components/AccessControl/LockDialog/ListLobbyPeer.js +++ b/app/src/components/AccessControl/LockDialog/ListLobbyPeer.js @@ -5,6 +5,8 @@ import PropTypes from 'prop-types'; import classnames from 'classnames'; import { withRoomContext } from '../../../RoomContext'; import { useIntl } from 'react-intl'; +import { permissions } from '../../../permissions'; +import { makePermissionSelector } from '../../Selectors'; import ListItem from '@material-ui/core/ListItem'; import ListItemText from '@material-ui/core/ListItemText'; import IconButton from '@material-ui/core/IconButton'; @@ -85,28 +87,32 @@ ListLobbyPeer.propTypes = classes : PropTypes.object.isRequired }; -const mapStateToProps = (state, { id }) => +const makeMapStateToProps = (initialState, { id }) => { - return { - peer : state.lobbyPeers[id], - promotionInProgress : state.room.lobbyPeersPromotionInProgress, - canPromote : - state.me.roles.some((role) => - state.room.permissionsFromRoles.PROMOTE_PEER.includes(role)) + const hasPermission = makePermissionSelector(permissions.PROMOTE_PEER); + + const mapStateToProps = (state) => + { + return { + peer : state.lobbyPeers[id], + promotionInProgress : state.room.lobbyPeersPromotionInProgress, + canPromote : hasPermission(state) + }; }; + + return mapStateToProps; }; export default withRoomContext(connect( - mapStateToProps, + makeMapStateToProps, null, null, { areStatesEqual : (next, prev) => { return ( - prev.room.permissionsFromRoles === next.room.permissionsFromRoles && - prev.room.lobbyPeersPromotionInProgress === - next.room.lobbyPeersPromotionInProgress && + prev.room === next.room && + prev.peers === next.peers && // For checking permissions prev.me.roles === next.me.roles && prev.lobbyPeers === next.lobbyPeers ); diff --git a/app/src/components/AccessControl/LockDialog/LockDialog.js b/app/src/components/AccessControl/LockDialog/LockDialog.js index 4d6cd24..c3dbd6a 100644 --- a/app/src/components/AccessControl/LockDialog/LockDialog.js +++ b/app/src/components/AccessControl/LockDialog/LockDialog.js @@ -1,8 +1,10 @@ import React from 'react'; import { connect } from 'react-redux'; import { - lobbyPeersKeySelector + lobbyPeersKeySelector, + makePermissionSelector } from '../../Selectors'; +import { permissions } from '../../../permissions'; import * as appPropTypes from '../../appPropTypes'; import { withStyles } from '@material-ui/core/styles'; import { withRoomContext } from '../../../RoomContext'; @@ -140,15 +142,20 @@ LockDialog.propTypes = classes : PropTypes.object.isRequired }; -const mapStateToProps = (state) => +const makeMapStateToProps = () => { - return { - room : state.room, - lobbyPeers : lobbyPeersKeySelector(state), - canPromote : - state.me.roles.some((role) => - state.room.permissionsFromRoles.PROMOTE_PEER.includes(role)) + const hasPermission = makePermissionSelector(permissions.PROMOTE_PEER); + + const mapStateToProps = (state) => + { + return { + room : state.room, + lobbyPeers : lobbyPeersKeySelector(state), + canPromote : hasPermission(state) + }; }; + + return mapStateToProps; }; const mapDispatchToProps = { @@ -157,7 +164,7 @@ const mapDispatchToProps = { }; export default withRoomContext(connect( - mapStateToProps, + makeMapStateToProps, mapDispatchToProps, null, { @@ -166,6 +173,7 @@ export default withRoomContext(connect( return ( prev.room === next.room && prev.me.roles === next.me.roles && + prev.peers === next.peers && prev.lobbyPeers === next.lobbyPeers ); } diff --git a/app/src/components/Containers/Me.js b/app/src/components/Containers/Me.js index ead95b1..aa3a9a3 100644 --- a/app/src/components/Containers/Me.js +++ b/app/src/components/Containers/Me.js @@ -1,6 +1,10 @@ import React, { useState } from 'react'; import { connect } from 'react-redux'; -import { meProducersSelector } from '../Selectors'; +import { + meProducersSelector, + makePermissionSelector +} from '../Selectors'; +import { permissions } from '../../permissions'; import { withRoomContext } from '../../RoomContext'; import { withStyles } from '@material-ui/core/styles'; import PropTypes from 'prop-types'; @@ -807,32 +811,37 @@ Me.propTypes = theme : PropTypes.object.isRequired }; -const mapStateToProps = (state) => +const makeMapStateToProps = () => { - return { - me : state.me, - ...meProducersSelector(state), - settings : state.settings, - activeSpeaker : state.me.id === state.room.activeSpeakerId, - canShareScreen : - state.me.roles.some((role) => - state.room.permissionsFromRoles.SHARE_SCREEN.includes(role)) + const hasPermission = makePermissionSelector(permissions.SHARE_SCREEN); + + const mapStateToProps = (state) => + { + return { + me : state.me, + ...meProducersSelector(state), + settings : state.settings, + activeSpeaker : state.me.id === state.room.activeSpeakerId, + canShareScreen : hasPermission(state) + }; }; + + return mapStateToProps; }; export default withRoomContext(connect( - mapStateToProps, + makeMapStateToProps, null, null, { areStatesEqual : (next, prev) => { return ( - prev.room.permissionsFromRoles === next.room.permissionsFromRoles && + prev.room === next.room && prev.me === next.me && + prev.peers === next.peers && prev.producers === next.producers && - prev.settings === next.settings && - prev.room.activeSpeakerId === next.room.activeSpeakerId + prev.settings === next.settings ); } } diff --git a/app/src/components/Controls/TopBar.js b/app/src/components/Controls/TopBar.js index 43ee13f..b880c36 100644 --- a/app/src/components/Controls/TopBar.js +++ b/app/src/components/Controls/TopBar.js @@ -4,8 +4,10 @@ import PropTypes from 'prop-types'; import { lobbyPeersKeySelector, peersLengthSelector, - raisedHandsSelector + raisedHandsSelector, + makePermissionSelector } from '../Selectors'; +import { permissions } from '../../permissions'; import * as appPropTypes from '../appPropTypes'; import { withRoomContext } from '../../RoomContext'; import { withStyles } from '@material-ui/core/styles'; @@ -751,27 +753,35 @@ TopBar.propTypes = theme : PropTypes.object.isRequired }; -const mapStateToProps = (state) => - ({ - room : state.room, - peersLength : peersLengthSelector(state), - lobbyPeers : lobbyPeersKeySelector(state), - permanentTopBar : state.settings.permanentTopBar, - loggedIn : state.me.loggedIn, - loginEnabled : state.me.loginEnabled, - myPicture : state.me.picture, - unread : state.toolarea.unreadMessages + - state.toolarea.unreadFiles + raisedHandsSelector(state), - canProduceExtraVideo : - state.me.roles.some((role) => - state.room.permissionsFromRoles.EXTRA_VIDEO.includes(role)), - canLock : - state.me.roles.some((role) => - state.room.permissionsFromRoles.CHANGE_ROOM_LOCK.includes(role)), - canPromote : - state.me.roles.some((role) => - state.room.permissionsFromRoles.PROMOTE_PEER.includes(role)) - }); +const makeMapStateToProps = () => +{ + const hasExtraVideoPermission = + makePermissionSelector(permissions.EXTRA_VIDEO); + + const hasLockPermission = + makePermissionSelector(permissions.CHANGE_ROOM_LOCK); + + const hasPromotionPermission = + makePermissionSelector(permissions.PROMOTE_PEER); + + const mapStateToProps = (state) => + ({ + room : state.room, + peersLength : peersLengthSelector(state), + lobbyPeers : lobbyPeersKeySelector(state), + permanentTopBar : state.settings.permanentTopBar, + loggedIn : state.me.loggedIn, + loginEnabled : state.me.loginEnabled, + myPicture : state.me.picture, + unread : state.toolarea.unreadMessages + + state.toolarea.unreadFiles + raisedHandsSelector(state), + canProduceExtraVideo : hasExtraVideoPermission(state), + canLock : hasLockPermission(state), + canPromote : hasPromotionPermission(state) + }); + + return mapStateToProps; +}; const mapDispatchToProps = (dispatch) => ({ @@ -811,7 +821,7 @@ const mapDispatchToProps = (dispatch) => }); export default withRoomContext(connect( - mapStateToProps, + makeMapStateToProps, mapDispatchToProps, null, { diff --git a/app/src/components/MeetingDrawer/Chat/ChatInput.js b/app/src/components/MeetingDrawer/Chat/ChatInput.js index 480bb26..bb07f98 100644 --- a/app/src/components/MeetingDrawer/Chat/ChatInput.js +++ b/app/src/components/MeetingDrawer/Chat/ChatInput.js @@ -4,6 +4,8 @@ import PropTypes from 'prop-types'; import { withStyles } from '@material-ui/core/styles'; import { withRoomContext } from '../../../RoomContext'; import { useIntl } from 'react-intl'; +import { permissions } from '../../../permissions'; +import { makePermissionSelector } from '../../Selectors'; import Paper from '@material-ui/core/Paper'; import InputBase from '@material-ui/core/InputBase'; import IconButton from '@material-ui/core/IconButton'; @@ -119,26 +121,32 @@ ChatInput.propTypes = classes : PropTypes.object.isRequired }; -const mapStateToProps = (state) => - ({ - displayName : state.settings.displayName, - picture : state.me.picture, - canChat : - state.me.roles.some((role) => - state.room.permissionsFromRoles.SEND_CHAT.includes(role)) - }); +const makeMapStateToProps = () => +{ + const hasPermission = makePermissionSelector(permissions.SEND_CHAT); + + const mapStateToProps = (state) => + ({ + displayName : state.settings.displayName, + picture : state.me.picture, + canChat : hasPermission(state) + }); + + return mapStateToProps; +}; export default withRoomContext( connect( - mapStateToProps, + makeMapStateToProps, null, null, { areStatesEqual : (next, prev) => { return ( - prev.room.permissionsFromRoles === next.room.permissionsFromRoles && + prev.room === next.room && prev.me.roles === next.me.roles && + prev.peers === next.peers && prev.settings.displayName === next.settings.displayName && prev.me.picture === next.me.picture ); diff --git a/app/src/components/MeetingDrawer/Chat/ChatModerator.js b/app/src/components/MeetingDrawer/Chat/ChatModerator.js index a35675b..d7b1cad 100644 --- a/app/src/components/MeetingDrawer/Chat/ChatModerator.js +++ b/app/src/components/MeetingDrawer/Chat/ChatModerator.js @@ -4,6 +4,8 @@ import PropTypes from 'prop-types'; import { withRoomContext } from '../../../RoomContext'; import { withStyles } from '@material-ui/core/styles'; import { useIntl, FormattedMessage } from 'react-intl'; +import { permissions } from '../../../permissions'; +import { makePermissionSelector } from '../../Selectors'; import Button from '@material-ui/core/Button'; const styles = (theme) => @@ -76,16 +78,21 @@ ChatModerator.propTypes = classes : PropTypes.object.isRequired }; -const mapStateToProps = (state) => - ({ - isChatModerator : - state.me.roles.some((role) => - state.room.permissionsFromRoles.MODERATE_CHAT.includes(role)), - room : state.room - }); +const makeMapStateToProps = () => +{ + const hasPermission = makePermissionSelector(permissions.MODERATE_CHAT); + + const mapStateToProps = (state) => + ({ + isChatModerator : hasPermission(state), + room : state.room + }); + + return mapStateToProps; +}; export default withRoomContext(connect( - mapStateToProps, + makeMapStateToProps, null, null, { @@ -93,7 +100,8 @@ export default withRoomContext(connect( { return ( prev.room === next.room && - prev.me === next.me + prev.me === next.me && + prev.peers === next.peers ); } } diff --git a/app/src/components/MeetingDrawer/FileSharing/FileSharing.js b/app/src/components/MeetingDrawer/FileSharing/FileSharing.js index 78ba569..8af1d8b 100644 --- a/app/src/components/MeetingDrawer/FileSharing/FileSharing.js +++ b/app/src/components/MeetingDrawer/FileSharing/FileSharing.js @@ -4,6 +4,8 @@ import { connect } from 'react-redux'; import { withStyles } from '@material-ui/core/styles'; import { withRoomContext } from '../../../RoomContext'; import { useIntl } from 'react-intl'; +import { permissions } from '../../../permissions'; +import { makePermissionSelector } from '../../Selectors'; import FileList from './FileList'; import FileSharingModerator from './FileSharingModerator'; import Paper from '@material-ui/core/Paper'; @@ -131,30 +133,36 @@ FileSharing.propTypes = { classes : PropTypes.object.isRequired }; -const mapStateToProps = (state) => +const makeMapStateToProps = () => { - return { - canShareFiles : state.me.canShareFiles, - browser : state.me.browser, - tabOpen : state.toolarea.currentToolTab === 'files', - canShare : - state.me.roles.some((role) => - state.room.permissionsFromRoles.SHARE_FILE.includes(role)) + const hasPermission = makePermissionSelector(permissions.SHARE_FILE); + + const mapStateToProps = (state) => + { + return { + canShareFiles : state.me.canShareFiles, + browser : state.me.browser, + tabOpen : state.toolarea.currentToolTab === 'files', + canShare : hasPermission(state) + }; }; + + return mapStateToProps; }; export default withRoomContext(connect( - mapStateToProps, + makeMapStateToProps, null, null, { areStatesEqual : (next, prev) => { return ( - prev.room.permissionsFromRoles === next.room.permissionsFromRoles && + prev.room === next.room && prev.me.browser === next.me.browser && prev.me.roles === next.me.roles && prev.me.canShareFiles === next.me.canShareFiles && + prev.peers === next.peers && prev.toolarea.currentToolTab === next.toolarea.currentToolTab ); } diff --git a/app/src/components/MeetingDrawer/FileSharing/FileSharingModerator.js b/app/src/components/MeetingDrawer/FileSharing/FileSharingModerator.js index e38e54c..05f35e8 100644 --- a/app/src/components/MeetingDrawer/FileSharing/FileSharingModerator.js +++ b/app/src/components/MeetingDrawer/FileSharing/FileSharingModerator.js @@ -4,6 +4,8 @@ import PropTypes from 'prop-types'; import { withRoomContext } from '../../../RoomContext'; import { withStyles } from '@material-ui/core/styles'; import { useIntl, FormattedMessage } from 'react-intl'; +import { permissions } from '../../../permissions'; +import { makePermissionSelector } from '../../Selectors'; import Button from '@material-ui/core/Button'; const styles = (theme) => @@ -76,16 +78,21 @@ FileSharingModerator.propTypes = classes : PropTypes.object.isRequired }; -const mapStateToProps = (state) => - ({ - isFileSharingModerator : - state.me.roles.some((role) => - state.room.permissionsFromRoles.MODERATE_FILES.includes(role)), - room : state.room - }); +const makeMapStateToProps = () => +{ + const hasPermission = makePermissionSelector(permissions.MODERATE_FILES); + + const mapStateToProps = (state) => + ({ + isFileSharingModerator : hasPermission(state), + room : state.room + }); + + return mapStateToProps; +}; export default withRoomContext(connect( - mapStateToProps, + makeMapStateToProps, null, null, { @@ -93,7 +100,8 @@ export default withRoomContext(connect( { return ( prev.room === next.room && - prev.me === next.me + prev.me === next.me && + prev.peers === next.peers ); } } diff --git a/app/src/components/MeetingDrawer/ParticipantList/ParticipantList.js b/app/src/components/MeetingDrawer/ParticipantList/ParticipantList.js index 07e5d24..a416a64 100644 --- a/app/src/components/MeetingDrawer/ParticipantList/ParticipantList.js +++ b/app/src/components/MeetingDrawer/ParticipantList/ParticipantList.js @@ -1,8 +1,10 @@ import React from 'react'; import { connect } from 'react-redux'; import { - participantListSelector + participantListSelector, + makePermissionSelector } from '../../Selectors'; +import { permissions } from '../../../permissions'; import classnames from 'classnames'; import { withStyles } from '@material-ui/core/styles'; import { withRoomContext } from '../../../RoomContext'; @@ -160,31 +162,34 @@ ParticipantList.propTypes = classes : PropTypes.object.isRequired }; -const mapStateToProps = (state) => +const makeMapStateToProps = () => { - return { - isModerator : - state.me.roles.some((role) => - state.room.permissionsFromRoles.MODERATE_ROOM.includes(role)), - participants : participantListSelector(state), - spotlights : state.room.spotlights, - selectedPeerId : state.room.selectedPeerId + const hasPermission = makePermissionSelector(permissions.MODERATE_ROOM); + + const mapStateToProps = (state) => + { + return { + isModerator : hasPermission(state), + participants : participantListSelector(state), + spotlights : state.room.spotlights, + selectedPeerId : state.room.selectedPeerId + }; }; + + return mapStateToProps; }; const ParticipantListContainer = withRoomContext(connect( - mapStateToProps, + makeMapStateToProps, null, null, { areStatesEqual : (next, prev) => { return ( - prev.room.permissionsFromRoles === next.room.permissionsFromRoles && + prev.room === next.room && prev.me.roles === next.me.roles && - prev.peers === next.peers && - prev.room.spotlights === next.room.spotlights && - prev.room.selectedPeerId === next.room.selectedPeerId + prev.peers === next.peers ); } } diff --git a/app/src/components/Selectors.js b/app/src/components/Selectors.js index b8e5e41..8ac1176 100644 --- a/app/src/components/Selectors.js +++ b/app/src/components/Selectors.js @@ -1,5 +1,8 @@ import { createSelector } from 'reselect'; +const meRolesSelect = (state) => state.me.roles; +const roomPermissionsSelect = (state) => state.room.roomPermissions; +const roomAllowWhenRoleMissing = (state) => state.room.allowWhenRoleMissing; const producersSelect = (state) => state.producers; const consumersSelect = (state) => state.consumers; const spotlightsSelector = (state) => state.room.spotlights; @@ -217,3 +220,53 @@ export const makePeerConsumerSelector = () => } ); }; + +// Very important that the Components that use this +// selector need to check at least these state changes: +// +// areStatesEqual : (next, prev) => +// { +// return ( +// prev.room.roomPermissions === next.room.roomPermissions && +// prev.room.allowWhenRoleMissing === next.room.allowWhenRoleMissing && +// prev.peers === next.peers && +// prev.me.roles === next.me.roles +// ); +// } +export const makePermissionSelector = (permission) => +{ + return createSelector( + meRolesSelect, + roomPermissionsSelect, + roomAllowWhenRoleMissing, + peersValueSelector, + (roles, roomPermissions, allowWhenRoleMissing, peers) => + { + if (!roomPermissions) + return false; + + const permitted = roles.some((role) => + roomPermissions[permission].includes(role) + ); + + if (permitted) + return true; + + if (!allowWhenRoleMissing) + return false; + + // Allow if config is set, and no one is present + if (allowWhenRoleMissing.includes(permission) && + peers.filter( + (peer) => + peer.roles.some( + (role) => roomPermissions[permission].includes(role) + ) + ).length === 0 + ) + return true; + + return false; + } + ); +}; \ No newline at end of file diff --git a/app/src/permissions.js b/app/src/permissions.js new file mode 100644 index 0000000..864bdbd --- /dev/null +++ b/app/src/permissions.js @@ -0,0 +1,20 @@ +export const permissions = { + // The role(s) have permission to lock/unlock a room + CHANGE_ROOM_LOCK : 'CHANGE_ROOM_LOCK', + // The role(s) have permission to promote a peer from the lobby + PROMOTE_PEER : 'PROMOTE_PEER', + // The role(s) have permission to send chat messages + SEND_CHAT : 'SEND_CHAT', + // The role(s) have permission to moderate chat + MODERATE_CHAT : 'MODERATE_CHAT', + // The role(s) have permission to share screen + SHARE_SCREEN : 'SHARE_SCREEN', + // The role(s) have permission to produce extra video + EXTRA_VIDEO : 'EXTRA_VIDEO', + // The role(s) have permission to share files + SHARE_FILE : 'SHARE_FILE', + // The role(s) have permission to moderate files + MODERATE_FILES : 'MODERATE_FILES', + // The role(s) have permission to moderate room (e.g. kick user) + MODERATE_ROOM : 'MODERATE_ROOM' +}; \ No newline at end of file diff --git a/app/src/reducers/room.js b/app/src/reducers/room.js index 1a4b99e..595ca4e 100644 --- a/app/src/reducers/room.js +++ b/app/src/reducers/room.js @@ -33,18 +33,8 @@ const initialState = closeMeetingInProgress : false, clearChatInProgress : false, clearFileSharingInProgress : false, - userRoles : { NORMAL: 'normal' }, // Default role - permissionsFromRoles : { - CHANGE_ROOM_LOCK : [], - PROMOTE_PEER : [], - SEND_CHAT : [], - MODERATE_CHAT : [], - SHARE_SCREEN : [], - EXTRA_VIDEO : [], - SHARE_FILE : [], - MODERATE_FILES : [], - MODERATE_ROOM : [] - } + roomPermissions : null, + allowWhenRoleMissing : null }; const room = (state = initialState, action) => @@ -240,18 +230,18 @@ const room = (state = initialState, action) => case 'CLEAR_FILE_SHARING_IN_PROGRESS': return { ...state, clearFileSharingInProgress: action.payload.flag }; - case 'SET_USER_ROLES': + case 'SET_ROOM_PERMISSIONS': { - const { userRoles } = action.payload; + const { roomPermissions } = action.payload; - return { ...state, userRoles }; + return { ...state, roomPermissions }; } - case 'SET_PERMISSIONS_FROM_ROLES': + case 'SET_ALLOW_WHEN_ROLE_MISSING': { - const { permissionsFromRoles } = action.payload; + const { allowWhenRoleMissing } = action.payload; - return { ...state, permissionsFromRoles }; + return { ...state, allowWhenRoleMissing }; } default: diff --git a/server/access.js b/server/access.js new file mode 100644 index 0000000..479e9e2 --- /dev/null +++ b/server/access.js @@ -0,0 +1,11 @@ +module.exports = { + // The role(s) will gain access to the room + // even if it is locked (!) + BYPASS_ROOM_LOCK : 'BYPASS_ROOM_LOCK', + // The role(s) will gain access to the room without + // going into the lobby. If you want to restrict access to your + // server to only directly allow authenticated users, you could + // add the userRoles.AUTHENTICATED to the user in the userMapping + // function, and change to BYPASS_LOBBY : [ userRoles.AUTHENTICATED ] + BYPASS_LOBBY : 'BYPASS_LOBBY' +}; \ No newline at end of file diff --git a/server/config/config.example.js b/server/config/config.example.js index dda9e2f..e08f910 100644 --- a/server/config/config.example.js +++ b/server/config/config.example.js @@ -1,5 +1,23 @@ const os = require('os'); const userRoles = require('../userRoles'); + +const { + BYPASS_ROOM_LOCK, + BYPASS_LOBBY +} = require('../access'); + +const { + CHANGE_ROOM_LOCK, + PROMOTE_PEER, + SEND_CHAT, + MODERATE_CHAT, + SHARE_SCREEN, + EXTRA_VIDEO, + SHARE_FILE, + MODERATE_FILES, + MODERATE_ROOM +} = require('../permissions'); + // const AwaitQueue = require('awaitqueue'); // const axios = require('axios'); @@ -216,44 +234,50 @@ module.exports = accessFromRoles : { // The role(s) will gain access to the room // even if it is locked (!) - BYPASS_ROOM_LOCK : [ userRoles.ADMIN ], + [BYPASS_ROOM_LOCK] : [ userRoles.ADMIN ], // The role(s) will gain access to the room without // going into the lobby. If you want to restrict access to your // server to only directly allow authenticated users, you could // add the userRoles.AUTHENTICATED to the user in the userMapping // function, and change to BYPASS_LOBBY : [ userRoles.AUTHENTICATED ] - BYPASS_LOBBY : [ userRoles.NORMAL ] + [BYPASS_LOBBY] : [ userRoles.NORMAL ] }, permissionsFromRoles : { // The role(s) have permission to lock/unlock a room - CHANGE_ROOM_LOCK : [ userRoles.NORMAL ], + [CHANGE_ROOM_LOCK] : [ userRoles.MODERATOR ], // The role(s) have permission to promote a peer from the lobby - PROMOTE_PEER : [ userRoles.NORMAL ], + [PROMOTE_PEER] : [ userRoles.NORMAL ], // The role(s) have permission to send chat messages - SEND_CHAT : [ userRoles.NORMAL ], + [SEND_CHAT] : [ userRoles.NORMAL ], // The role(s) have permission to moderate chat - MODERATE_CHAT : [ userRoles.MODERATOR ], + [MODERATE_CHAT] : [ userRoles.MODERATOR ], // The role(s) have permission to share screen - SHARE_SCREEN : [ userRoles.NORMAL ], + [SHARE_SCREEN] : [ userRoles.NORMAL ], // The role(s) have permission to produce extra video - EXTRA_VIDEO : [ userRoles.NORMAL ], + [EXTRA_VIDEO] : [ userRoles.NORMAL ], // The role(s) have permission to share files - SHARE_FILE : [ userRoles.NORMAL ], + [SHARE_FILE] : [ userRoles.NORMAL ], // The role(s) have permission to moderate files - MODERATE_FILES : [ userRoles.MODERATOR ], + [MODERATE_FILES] : [ userRoles.MODERATOR ], // The role(s) have permission to moderate room (e.g. kick user) - MODERATE_ROOM : [ userRoles.MODERATOR ] + [MODERATE_ROOM] : [ userRoles.MODERATOR ] }, + // Array of permissions. If no peer with the permission in question + // is in the room, all peers are permitted to do the action. The peers + // that are allowed because of this rule will not be able to do this + // action as soon as a peer with the permission joins. In this example + // everyone will be able to lock/unlock room until a MODERATOR joins. + allowWhenRoleMissing : [ CHANGE_ROOM_LOCK ], // When truthy, the room will be open to all users when as long as there // are allready users in the room - activateOnHostJoin : true, + activateOnHostJoin : true, // When set, maxUsersPerRoom defines how many users can join // a single room. If not set, there is no limit. // maxUsersPerRoom : 20, // Room size before spreading to new router - routerScaleSize : 40, + routerScaleSize : 40, // Mediasoup settings - mediasoup : + mediasoup : { numWorkers : Object.keys(os.cpus()).length, // mediasoup Worker settings. diff --git a/server/lib/Room.js b/server/lib/Room.js index 77f00ee..3ab281b 100644 --- a/server/lib/Room.js +++ b/server/lib/Room.js @@ -5,29 +5,47 @@ const Lobby = require('./Lobby'); const { v4: uuidv4 } = require('uuid'); const jwt = require('jsonwebtoken'); const userRoles = require('../userRoles'); + +const { + BYPASS_ROOM_LOCK, + BYPASS_LOBBY +} = require('../access'); + +const permissions = require('../permissions'), { + CHANGE_ROOM_LOCK, + PROMOTE_PEER, + SEND_CHAT, + MODERATE_CHAT, + SHARE_SCREEN, + EXTRA_VIDEO, + SHARE_FILE, + MODERATE_FILES, + MODERATE_ROOM +} = permissions; + const config = require('../config/config'); const logger = new Logger('Room'); // In case they are not configured properly -const accessFromRoles = +const roomAccess = { - BYPASS_ROOM_LOCK : [ userRoles.ADMIN ], - BYPASS_LOBBY : [ userRoles.NORMAL ], + [BYPASS_ROOM_LOCK] : [ userRoles.ADMIN ], + [BYPASS_LOBBY] : [ userRoles.NORMAL ], ...config.accessFromRoles }; -const permissionsFromRoles = +const roomPermissions = { - CHANGE_ROOM_LOCK : [ userRoles.NORMAL ], - PROMOTE_PEER : [ userRoles.NORMAL ], - SEND_CHAT : [ userRoles.NORMAL ], - MODERATE_CHAT : [ userRoles.MODERATOR ], - SHARE_SCREEN : [ userRoles.NORMAL ], - EXTRA_VIDEO : [ userRoles.NORMAL ], - SHARE_FILE : [ userRoles.NORMAL ], - MODERATE_FILES : [ userRoles.MODERATOR ], - MODERATE_ROOM : [ userRoles.MODERATOR ], + [CHANGE_ROOM_LOCK] : [ userRoles.NORMAL ], + [PROMOTE_PEER] : [ userRoles.NORMAL ], + [SEND_CHAT] : [ userRoles.NORMAL ], + [MODERATE_CHAT] : [ userRoles.MODERATOR ], + [SHARE_SCREEN] : [ userRoles.NORMAL ], + [EXTRA_VIDEO] : [ userRoles.NORMAL ], + [SHARE_FILE] : [ userRoles.NORMAL ], + [MODERATE_FILES] : [ userRoles.MODERATOR ], + [MODERATE_ROOM] : [ userRoles.MODERATOR ], ...config.permissionsFromRoles }; @@ -221,9 +239,8 @@ class Room extends EventEmitter // Returning user if (returning) this._peerJoining(peer, true); - else if ( // Has a role that is allowed to bypass room lock - peer.roles.some((role) => accessFromRoles.BYPASS_ROOM_LOCK.includes(role)) - ) + // Has a role that is allowed to bypass room lock + else if (this._hasAccess(peer, BYPASS_ROOM_LOCK)) this._peerJoining(peer); else if ( 'maxUsersPerRoom' in config && @@ -239,7 +256,7 @@ class Room extends EventEmitter else { // Has a role that is allowed to bypass lobby - peer.roles.some((role) => accessFromRoles.BYPASS_LOBBY.includes(role)) ? + this._hasAccess(peer, BYPASS_LOBBY) ? this._peerJoining(peer) : this._handleGuest(peer); } @@ -271,11 +288,7 @@ class Room extends EventEmitter this._peerJoining(promotedPeer); - for ( - const peer of this._getPeersWithPermission({ - permission : permissionsFromRoles.PROMOTE_PEER - }) - ) + for (const peer of this._getPeersWithPermission(PROMOTE_PEER)) { this._notification(peer.socket, 'lobby:promotedPeer', { peerId: id }); } @@ -283,9 +296,8 @@ class Room extends EventEmitter this._lobby.on('peerRolesChanged', (peer) => { - if ( // Has a role that is allowed to bypass room lock - peer.roles.some((role) => accessFromRoles.BYPASS_ROOM_LOCK.includes(role)) - ) + // Has a role that is allowed to bypass room lock + if (this._hasAccess(peer, BYPASS_ROOM_LOCK)) { this._lobby.promotePeer(peer.id); @@ -294,7 +306,7 @@ class Room extends EventEmitter if ( // Has a role that is allowed to bypass lobby !this._locked && - peer.roles.some((role) => accessFromRoles.BYPASS_LOBBY.includes(role)) + this._hasAccess(peer, BYPASS_LOBBY) ) { this._lobby.promotePeer(peer.id); @@ -307,11 +319,7 @@ class Room extends EventEmitter { const { id, displayName } = changedPeer; - for ( - const peer of this._getPeersWithPermission({ - permission : permissionsFromRoles.PROMOTE_PEER - }) - ) + for (const peer of this._getPeersWithPermission(PROMOTE_PEER)) { this._notification(peer.socket, 'lobby:changeDisplayName', { peerId: id, displayName }); } @@ -321,11 +329,7 @@ class Room extends EventEmitter { const { id, picture } = changedPeer; - for ( - const peer of this._getPeersWithPermission({ - permission : permissionsFromRoles.PROMOTE_PEER - }) - ) + for (const peer of this._getPeersWithPermission(PROMOTE_PEER)) { this._notification(peer.socket, 'lobby:changePicture', { peerId: id, picture }); } @@ -337,11 +341,7 @@ class Room extends EventEmitter const { id } = closedPeer; - for ( - const peer of this._getPeersWithPermission({ - permission : permissionsFromRoles.PROMOTE_PEER - }) - ) + for (const peer of this._getPeersWithPermission(PROMOTE_PEER)) { this._notification(peer.socket, 'lobby:peerClosed', { peerId: id }); } @@ -401,7 +401,7 @@ class Room extends EventEmitter ); } - async dump() + dump() { return { roomId : this._roomId, @@ -447,11 +447,7 @@ class Room extends EventEmitter { this._lobby.parkPeer(parkPeer); - for ( - const peer of this._getPeersWithPermission({ - permission : permissionsFromRoles.PROMOTE_PEER - }) - ) + for (const peer of this._getPeersWithPermission(PROMOTE_PEER)) { this._notification(peer.socket, 'parkedPeer', { peerId: parkPeer.id }); } @@ -602,7 +598,7 @@ class Room extends EventEmitter // Got permission to promote peers, notify peer of // peers in lobby - if (permissionsFromRoles.PROMOTE_PEER.includes(newRole)) + if (roomPermissions.PROMOTE_PEER.includes(newRole)) { const lobbyPeers = this._lobby.peerList(); @@ -670,12 +666,9 @@ class Room extends EventEmitter .map((joinedPeer) => (joinedPeer.peerInfo)); let lobbyPeers = []; - - if ( // Allowed to promote peers, notify about lobbypeers - peer.roles.some((role) => - permissionsFromRoles.PROMOTE_PEER.includes(role) - ) - ) + + // Allowed to promote peers, notify about lobbypeers + if (this._hasPermission(peer, PROMOTE_PEER)) lobbyPeers = this._lobby.peerList(); cb(null, { @@ -683,8 +676,9 @@ class Room extends EventEmitter peers : peerInfos, tracker : config.fileTracker, authenticated : peer.authenticated, - permissionsFromRoles : permissionsFromRoles, + roomPermissions : roomPermissions, userRoles : userRoles, + allowWhenRoleMissing : config.allowWhenRoleMissing, chatHistory : this._chatHistory, fileHistory : this._fileHistory, lastNHistory : this._lastN, @@ -711,7 +705,7 @@ class Room extends EventEmitter } // Notify the new Peer to all other Peers. - for (const otherPeer of this._getJoinedPeers({ excludePeer: peer })) + for (const otherPeer of this._getJoinedPeers(peer)) { this._notification( otherPeer.socket, @@ -821,15 +815,13 @@ class Room extends EventEmitter if ( appData.source === 'screen' && - !peer.roles.some( - (role) => permissionsFromRoles.SHARE_SCREEN.includes(role)) + !this._hasPermission(peer, SHARE_SCREEN) ) throw new Error('peer not authorized'); if ( appData.source === 'extravideo' && - !peer.roles.some( - (role) => permissionsFromRoles.EXTRA_VIDEO.includes(role)) + !this._hasPermission(peer, EXTRA_VIDEO) ) throw new Error('peer not authorized'); @@ -882,7 +874,7 @@ class Room extends EventEmitter cb(null, { id: producer.id }); // Optimization: Create a server-side Consumer for each Peer. - for (const otherPeer of this._getJoinedPeers({ excludePeer: peer })) + for (const otherPeer of this._getJoinedPeers(peer)) { this._createConsumer( { @@ -1144,9 +1136,7 @@ class Room extends EventEmitter case 'chatMessage': { - if ( - !peer.roles.some((role) => permissionsFromRoles.SEND_CHAT.includes(role)) - ) + if (!this._hasPermission(peer, SEND_CHAT)) throw new Error('peer not authorized'); const { chatMessage } = request.data; @@ -1167,11 +1157,7 @@ class Room extends EventEmitter case 'moderator:clearChat': { - if ( - !peer.roles.some( - (role) => permissionsFromRoles.MODERATE_CHAT.includes(role) - ) - ) + if (!this._hasPermission(peer, MODERATE_CHAT)) throw new Error('peer not authorized'); this._chatHistory = []; @@ -1187,11 +1173,7 @@ class Room extends EventEmitter case 'lockRoom': { - if ( - !peer.roles.some( - (role) => permissionsFromRoles.CHANGE_ROOM_LOCK.includes(role) - ) - ) + if (!this._hasPermission(peer, CHANGE_ROOM_LOCK)) throw new Error('peer not authorized'); this._locked = true; @@ -1209,11 +1191,7 @@ class Room extends EventEmitter case 'unlockRoom': { - if ( - !peer.roles.some( - (role) => permissionsFromRoles.CHANGE_ROOM_LOCK.includes(role) - ) - ) + if (!this._hasPermission(peer, CHANGE_ROOM_LOCK)) throw new Error('peer not authorized'); this._locked = false; @@ -1271,11 +1249,7 @@ class Room extends EventEmitter case 'promotePeer': { - if ( - !peer.roles.some( - (role) => permissionsFromRoles.PROMOTE_PEER.includes(role) - ) - ) + if (!this._hasPermission(peer, PROMOTE_PEER)) throw new Error('peer not authorized'); const { peerId } = request.data; @@ -1290,11 +1264,7 @@ class Room extends EventEmitter case 'promoteAllPeers': { - if ( - !peer.roles.some( - (role) => permissionsFromRoles.PROMOTE_PEER.includes(role) - ) - ) + if (!this._hasPermission(peer, PROMOTE_PEER)) throw new Error('peer not authorized'); this._lobby.promoteAllPeers(); @@ -1307,11 +1277,7 @@ class Room extends EventEmitter case 'sendFile': { - if ( - !peer.roles.some( - (role) => permissionsFromRoles.SHARE_FILE.includes(role) - ) - ) + if (!this._hasPermission(peer, SHARE_FILE)) throw new Error('peer not authorized'); const { magnetUri } = request.data; @@ -1332,11 +1298,7 @@ class Room extends EventEmitter case 'moderator:clearFileSharing': { - if ( - !peer.roles.some( - (role) => permissionsFromRoles.MODERATE_FILES.includes(role) - ) - ) + if (!this._hasPermission(peer, MODERATE_FILES)) throw new Error('peer not authorized'); this._fileHistory = []; @@ -1371,11 +1333,7 @@ class Room extends EventEmitter case 'moderator:mute': { - if ( - !peer.roles.some( - (role) => permissionsFromRoles.MODERATE_ROOM.includes(role) - ) - ) + if (!this._hasPermission(peer, MODERATE_ROOM)) throw new Error('peer not authorized'); const { peerId } = request.data; @@ -1394,11 +1352,7 @@ class Room extends EventEmitter case 'moderator:muteAll': { - if ( - !peer.roles.some( - (role) => permissionsFromRoles.MODERATE_ROOM.includes(role) - ) - ) + if (!this._hasPermission(peer, MODERATE_ROOM)) throw new Error('peer not authorized'); // Spread to others @@ -1411,11 +1365,7 @@ class Room extends EventEmitter case 'moderator:stopVideo': { - if ( - !peer.roles.some( - (role) => permissionsFromRoles.MODERATE_ROOM.includes(role) - ) - ) + if (!this._hasPermission(peer, MODERATE_ROOM)) throw new Error('peer not authorized'); const { peerId } = request.data; @@ -1434,11 +1384,7 @@ class Room extends EventEmitter case 'moderator:stopAllVideo': { - if ( - !peer.roles.some( - (role) => permissionsFromRoles.MODERATE_ROOM.includes(role) - ) - ) + if (!this._hasPermission(peer, MODERATE_ROOM)) throw new Error('peer not authorized'); // Spread to others @@ -1451,11 +1397,7 @@ class Room extends EventEmitter case 'moderator:closeMeeting': { - if ( - !peer.roles.some( - (role) => permissionsFromRoles.MODERATE_ROOM.includes(role) - ) - ) + if (!this._hasPermission(peer, MODERATE_ROOM)) throw new Error('peer not authorized'); this._notification(peer.socket, 'moderator:kick', null, true); @@ -1470,11 +1412,7 @@ class Room extends EventEmitter case 'moderator:kickPeer': { - if ( - !peer.roles.some( - (role) => permissionsFromRoles.MODERATE_ROOM.includes(role) - ) - ) + if (!this._hasPermission(peer, MODERATE_ROOM)) throw new Error('peer not authorized'); const { peerId } = request.data; @@ -1495,11 +1433,7 @@ class Room extends EventEmitter case 'moderator:lowerHand': { - if ( - !peer.roles.some( - (role) => permissionsFromRoles.MODERATE_ROOM.includes(role) - ) - ) + if (!this._hasPermission(peer, MODERATE_ROOM)) throw new Error('peer not authorized'); const { peerId } = request.data; @@ -1677,16 +1611,41 @@ class Room extends EventEmitter } } + _hasPermission(peer, permission) + { + const hasPermission = peer.roles.some((role) => + roomPermissions[permission].includes(role) + ); + + if (hasPermission) + return true; + + // Allow if config is set, and no one is present + if ( + 'allowWhenRoleMissing' in config && + config.allowWhenRoleMissing.includes(permission) && + this._getPeersWithPermission(permission).length === 0 + ) + return true; + + return false; + } + + _hasAccess(peer, access) + { + return peer.roles.some((role) => roomAccess[access].includes(role)); + } + /** * Helper to get the list of joined peers. */ - _getJoinedPeers({ excludePeer = undefined } = {}) + _getJoinedPeers(excludePeer = undefined) { return Object.values(this._peers) .filter((peer) => peer.joined && peer !== excludePeer); } - _getPeersWithPermission({ permission = null, excludePeer = undefined, joined = true }) + _getPeersWithPermission(permission = null, excludePeer = undefined, joined = true) { return Object.values(this._peers) .filter( @@ -1694,7 +1653,7 @@ class Room extends EventEmitter peer.joined === joined && peer !== excludePeer && peer.roles.some( - (role) => permission.includes(role) + (role) => roomPermissions[permission].includes(role) ) ); } diff --git a/server/permissions.js b/server/permissions.js new file mode 100644 index 0000000..dd3bdbb --- /dev/null +++ b/server/permissions.js @@ -0,0 +1,20 @@ +module.exports = { + // The role(s) have permission to lock/unlock a room + CHANGE_ROOM_LOCK : 'CHANGE_ROOM_LOCK', + // The role(s) have permission to promote a peer from the lobby + PROMOTE_PEER : 'PROMOTE_PEER', + // The role(s) have permission to send chat messages + SEND_CHAT : 'SEND_CHAT', + // The role(s) have permission to moderate chat + MODERATE_CHAT : 'MODERATE_CHAT', + // The role(s) have permission to share screen + SHARE_SCREEN : 'SHARE_SCREEN', + // The role(s) have permission to produce extra video + EXTRA_VIDEO : 'EXTRA_VIDEO', + // The role(s) have permission to share files + SHARE_FILE : 'SHARE_FILE', + // The role(s) have permission to moderate files + MODERATE_FILES : 'MODERATE_FILES', + // The role(s) have permission to moderate room (e.g. kick user) + MODERATE_ROOM : 'MODERATE_ROOM' +}; \ No newline at end of file