New option for handling permissions in rooms. Set allowWhenRoleMissing to permit actions before a peer with that permission joins. Ref #303

auto_join_3.3
Håvar Aambø Fosstveit 2020-05-08 16:19:55 +02:00
parent 220d4dd99d
commit a49258e840
18 changed files with 437 additions and 288 deletions

View File

@ -2847,8 +2847,8 @@ export default class RoomClient
roles, roles,
peers, peers,
tracker, tracker,
permissionsFromRoles, roomPermissions,
userRoles, allowWhenRoleMissing,
chatHistory, chatHistory,
fileHistory, fileHistory,
lastNHistory, lastNHistory,
@ -2874,8 +2874,10 @@ export default class RoomClient
store.dispatch(meActions.loggedIn(authenticated)); store.dispatch(meActions.loggedIn(authenticated));
store.dispatch(roomActions.setUserRoles(userRoles)); store.dispatch(roomActions.setRoomPermissions(roomPermissions));
store.dispatch(roomActions.setPermissionsFromRoles(permissionsFromRoles));
if (allowWhenRoleMissing)
store.dispatch(roomActions.setAllowWhenRoleMissing(allowWhenRoleMissing));
const myRoles = store.getState().me.roles; const myRoles = store.getState().me.roles;

View File

@ -177,14 +177,14 @@ export const setClearFileSharingInProgress = (flag) =>
payload : { flag } payload : { flag }
}); });
export const setUserRoles = (userRoles) => export const setRoomPermissions = (roomPermissions) =>
({ ({
type : 'SET_USER_ROLES', type : 'SET_ROOM_PERMISSIONS',
payload : { userRoles } payload : { roomPermissions }
}); });
export const setPermissionsFromRoles = (permissionsFromRoles) => export const setAllowWhenRoleMissing = (allowWhenRoleMissing) =>
({ ({
type : 'SET_PERMISSIONS_FROM_ROLES', type : 'SET_ALLOW_WHEN_ROLE_MISSING',
payload : { permissionsFromRoles } payload : { allowWhenRoleMissing }
}); });

View File

@ -5,6 +5,8 @@ import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import { withRoomContext } from '../../../RoomContext'; import { withRoomContext } from '../../../RoomContext';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { permissions } from '../../../permissions';
import { makePermissionSelector } from '../../Selectors';
import ListItem from '@material-ui/core/ListItem'; import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText'; import ListItemText from '@material-ui/core/ListItemText';
import IconButton from '@material-ui/core/IconButton'; import IconButton from '@material-ui/core/IconButton';
@ -85,28 +87,32 @@ ListLobbyPeer.propTypes =
classes : PropTypes.object.isRequired classes : PropTypes.object.isRequired
}; };
const mapStateToProps = (state, { id }) => const makeMapStateToProps = (initialState, { id }) =>
{
const hasPermission = makePermissionSelector(permissions.PROMOTE_PEER);
const mapStateToProps = (state) =>
{ {
return { return {
peer : state.lobbyPeers[id], peer : state.lobbyPeers[id],
promotionInProgress : state.room.lobbyPeersPromotionInProgress, promotionInProgress : state.room.lobbyPeersPromotionInProgress,
canPromote : canPromote : hasPermission(state)
state.me.roles.some((role) =>
state.room.permissionsFromRoles.PROMOTE_PEER.includes(role))
}; };
}; };
return mapStateToProps;
};
export default withRoomContext(connect( export default withRoomContext(connect(
mapStateToProps, makeMapStateToProps,
null, null,
null, null,
{ {
areStatesEqual : (next, prev) => areStatesEqual : (next, prev) =>
{ {
return ( return (
prev.room.permissionsFromRoles === next.room.permissionsFromRoles && prev.room === next.room &&
prev.room.lobbyPeersPromotionInProgress === prev.peers === next.peers && // For checking permissions
next.room.lobbyPeersPromotionInProgress &&
prev.me.roles === next.me.roles && prev.me.roles === next.me.roles &&
prev.lobbyPeers === next.lobbyPeers prev.lobbyPeers === next.lobbyPeers
); );

View File

@ -1,8 +1,10 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
lobbyPeersKeySelector lobbyPeersKeySelector,
makePermissionSelector
} from '../../Selectors'; } from '../../Selectors';
import { permissions } from '../../../permissions';
import * as appPropTypes from '../../appPropTypes'; import * as appPropTypes from '../../appPropTypes';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../../RoomContext'; import { withRoomContext } from '../../../RoomContext';
@ -140,24 +142,29 @@ LockDialog.propTypes =
classes : PropTypes.object.isRequired classes : PropTypes.object.isRequired
}; };
const makeMapStateToProps = () =>
{
const hasPermission = makePermissionSelector(permissions.PROMOTE_PEER);
const mapStateToProps = (state) => const mapStateToProps = (state) =>
{ {
return { return {
room : state.room, room : state.room,
lobbyPeers : lobbyPeersKeySelector(state), lobbyPeers : lobbyPeersKeySelector(state),
canPromote : canPromote : hasPermission(state)
state.me.roles.some((role) =>
state.room.permissionsFromRoles.PROMOTE_PEER.includes(role))
}; };
}; };
return mapStateToProps;
};
const mapDispatchToProps = { const mapDispatchToProps = {
handleCloseLockDialog : roomActions.setLockDialogOpen, handleCloseLockDialog : roomActions.setLockDialogOpen,
handleAccessCode : roomActions.setAccessCode handleAccessCode : roomActions.setAccessCode
}; };
export default withRoomContext(connect( export default withRoomContext(connect(
mapStateToProps, makeMapStateToProps,
mapDispatchToProps, mapDispatchToProps,
null, null,
{ {
@ -166,6 +173,7 @@ export default withRoomContext(connect(
return ( return (
prev.room === next.room && prev.room === next.room &&
prev.me.roles === next.me.roles && prev.me.roles === next.me.roles &&
prev.peers === next.peers &&
prev.lobbyPeers === next.lobbyPeers prev.lobbyPeers === next.lobbyPeers
); );
} }

View File

@ -1,6 +1,10 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { meProducersSelector } from '../Selectors'; import {
meProducersSelector,
makePermissionSelector
} from '../Selectors';
import { permissions } from '../../permissions';
import { withRoomContext } from '../../RoomContext'; import { withRoomContext } from '../../RoomContext';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@ -807,6 +811,10 @@ Me.propTypes =
theme : PropTypes.object.isRequired theme : PropTypes.object.isRequired
}; };
const makeMapStateToProps = () =>
{
const hasPermission = makePermissionSelector(permissions.SHARE_SCREEN);
const mapStateToProps = (state) => const mapStateToProps = (state) =>
{ {
return { return {
@ -814,25 +822,26 @@ const mapStateToProps = (state) =>
...meProducersSelector(state), ...meProducersSelector(state),
settings : state.settings, settings : state.settings,
activeSpeaker : state.me.id === state.room.activeSpeakerId, activeSpeaker : state.me.id === state.room.activeSpeakerId,
canShareScreen : canShareScreen : hasPermission(state)
state.me.roles.some((role) =>
state.room.permissionsFromRoles.SHARE_SCREEN.includes(role))
}; };
}; };
return mapStateToProps;
};
export default withRoomContext(connect( export default withRoomContext(connect(
mapStateToProps, makeMapStateToProps,
null, null,
null, null,
{ {
areStatesEqual : (next, prev) => areStatesEqual : (next, prev) =>
{ {
return ( return (
prev.room.permissionsFromRoles === next.room.permissionsFromRoles && prev.room === next.room &&
prev.me === next.me && prev.me === next.me &&
prev.peers === next.peers &&
prev.producers === next.producers && prev.producers === next.producers &&
prev.settings === next.settings && prev.settings === next.settings
prev.room.activeSpeakerId === next.room.activeSpeakerId
); );
} }
} }

View File

@ -4,8 +4,10 @@ import PropTypes from 'prop-types';
import { import {
lobbyPeersKeySelector, lobbyPeersKeySelector,
peersLengthSelector, peersLengthSelector,
raisedHandsSelector raisedHandsSelector,
makePermissionSelector
} from '../Selectors'; } from '../Selectors';
import { permissions } from '../../permissions';
import * as appPropTypes from '../appPropTypes'; import * as appPropTypes from '../appPropTypes';
import { withRoomContext } from '../../RoomContext'; import { withRoomContext } from '../../RoomContext';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
@ -751,6 +753,17 @@ TopBar.propTypes =
theme : PropTypes.object.isRequired theme : PropTypes.object.isRequired
}; };
const makeMapStateToProps = () =>
{
const hasExtraVideoPermission =
makePermissionSelector(permissions.EXTRA_VIDEO);
const hasLockPermission =
makePermissionSelector(permissions.CHANGE_ROOM_LOCK);
const hasPromotionPermission =
makePermissionSelector(permissions.PROMOTE_PEER);
const mapStateToProps = (state) => const mapStateToProps = (state) =>
({ ({
room : state.room, room : state.room,
@ -762,17 +775,14 @@ const mapStateToProps = (state) =>
myPicture : state.me.picture, myPicture : state.me.picture,
unread : state.toolarea.unreadMessages + unread : state.toolarea.unreadMessages +
state.toolarea.unreadFiles + raisedHandsSelector(state), state.toolarea.unreadFiles + raisedHandsSelector(state),
canProduceExtraVideo : canProduceExtraVideo : hasExtraVideoPermission(state),
state.me.roles.some((role) => canLock : hasLockPermission(state),
state.room.permissionsFromRoles.EXTRA_VIDEO.includes(role)), canPromote : hasPromotionPermission(state)
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))
}); });
return mapStateToProps;
};
const mapDispatchToProps = (dispatch) => const mapDispatchToProps = (dispatch) =>
({ ({
setToolbarsVisible : (visible) => setToolbarsVisible : (visible) =>
@ -811,7 +821,7 @@ const mapDispatchToProps = (dispatch) =>
}); });
export default withRoomContext(connect( export default withRoomContext(connect(
mapStateToProps, makeMapStateToProps,
mapDispatchToProps, mapDispatchToProps,
null, null,
{ {

View File

@ -4,6 +4,8 @@ import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../../RoomContext'; import { withRoomContext } from '../../../RoomContext';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { permissions } from '../../../permissions';
import { makePermissionSelector } from '../../Selectors';
import Paper from '@material-ui/core/Paper'; import Paper from '@material-ui/core/Paper';
import InputBase from '@material-ui/core/InputBase'; import InputBase from '@material-ui/core/InputBase';
import IconButton from '@material-ui/core/IconButton'; import IconButton from '@material-ui/core/IconButton';
@ -119,26 +121,32 @@ ChatInput.propTypes =
classes : PropTypes.object.isRequired classes : PropTypes.object.isRequired
}; };
const makeMapStateToProps = () =>
{
const hasPermission = makePermissionSelector(permissions.SEND_CHAT);
const mapStateToProps = (state) => const mapStateToProps = (state) =>
({ ({
displayName : state.settings.displayName, displayName : state.settings.displayName,
picture : state.me.picture, picture : state.me.picture,
canChat : canChat : hasPermission(state)
state.me.roles.some((role) =>
state.room.permissionsFromRoles.SEND_CHAT.includes(role))
}); });
return mapStateToProps;
};
export default withRoomContext( export default withRoomContext(
connect( connect(
mapStateToProps, makeMapStateToProps,
null, null,
null, null,
{ {
areStatesEqual : (next, prev) => areStatesEqual : (next, prev) =>
{ {
return ( return (
prev.room.permissionsFromRoles === next.room.permissionsFromRoles && prev.room === next.room &&
prev.me.roles === next.me.roles && prev.me.roles === next.me.roles &&
prev.peers === next.peers &&
prev.settings.displayName === next.settings.displayName && prev.settings.displayName === next.settings.displayName &&
prev.me.picture === next.me.picture prev.me.picture === next.me.picture
); );

View File

@ -4,6 +4,8 @@ import PropTypes from 'prop-types';
import { withRoomContext } from '../../../RoomContext'; import { withRoomContext } from '../../../RoomContext';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import { useIntl, FormattedMessage } from 'react-intl'; import { useIntl, FormattedMessage } from 'react-intl';
import { permissions } from '../../../permissions';
import { makePermissionSelector } from '../../Selectors';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
const styles = (theme) => const styles = (theme) =>
@ -76,16 +78,21 @@ ChatModerator.propTypes =
classes : PropTypes.object.isRequired classes : PropTypes.object.isRequired
}; };
const makeMapStateToProps = () =>
{
const hasPermission = makePermissionSelector(permissions.MODERATE_CHAT);
const mapStateToProps = (state) => const mapStateToProps = (state) =>
({ ({
isChatModerator : isChatModerator : hasPermission(state),
state.me.roles.some((role) =>
state.room.permissionsFromRoles.MODERATE_CHAT.includes(role)),
room : state.room room : state.room
}); });
return mapStateToProps;
};
export default withRoomContext(connect( export default withRoomContext(connect(
mapStateToProps, makeMapStateToProps,
null, null,
null, null,
{ {
@ -93,7 +100,8 @@ export default withRoomContext(connect(
{ {
return ( return (
prev.room === next.room && prev.room === next.room &&
prev.me === next.me prev.me === next.me &&
prev.peers === next.peers
); );
} }
} }

View File

@ -4,6 +4,8 @@ import { connect } from 'react-redux';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../../RoomContext'; import { withRoomContext } from '../../../RoomContext';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { permissions } from '../../../permissions';
import { makePermissionSelector } from '../../Selectors';
import FileList from './FileList'; import FileList from './FileList';
import FileSharingModerator from './FileSharingModerator'; import FileSharingModerator from './FileSharingModerator';
import Paper from '@material-ui/core/Paper'; import Paper from '@material-ui/core/Paper';
@ -131,30 +133,36 @@ FileSharing.propTypes = {
classes : PropTypes.object.isRequired classes : PropTypes.object.isRequired
}; };
const makeMapStateToProps = () =>
{
const hasPermission = makePermissionSelector(permissions.SHARE_FILE);
const mapStateToProps = (state) => const mapStateToProps = (state) =>
{ {
return { return {
canShareFiles : state.me.canShareFiles, canShareFiles : state.me.canShareFiles,
browser : state.me.browser, browser : state.me.browser,
tabOpen : state.toolarea.currentToolTab === 'files', tabOpen : state.toolarea.currentToolTab === 'files',
canShare : canShare : hasPermission(state)
state.me.roles.some((role) =>
state.room.permissionsFromRoles.SHARE_FILE.includes(role))
}; };
}; };
return mapStateToProps;
};
export default withRoomContext(connect( export default withRoomContext(connect(
mapStateToProps, makeMapStateToProps,
null, null,
null, null,
{ {
areStatesEqual : (next, prev) => areStatesEqual : (next, prev) =>
{ {
return ( return (
prev.room.permissionsFromRoles === next.room.permissionsFromRoles && prev.room === next.room &&
prev.me.browser === next.me.browser && prev.me.browser === next.me.browser &&
prev.me.roles === next.me.roles && prev.me.roles === next.me.roles &&
prev.me.canShareFiles === next.me.canShareFiles && prev.me.canShareFiles === next.me.canShareFiles &&
prev.peers === next.peers &&
prev.toolarea.currentToolTab === next.toolarea.currentToolTab prev.toolarea.currentToolTab === next.toolarea.currentToolTab
); );
} }

View File

@ -4,6 +4,8 @@ import PropTypes from 'prop-types';
import { withRoomContext } from '../../../RoomContext'; import { withRoomContext } from '../../../RoomContext';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import { useIntl, FormattedMessage } from 'react-intl'; import { useIntl, FormattedMessage } from 'react-intl';
import { permissions } from '../../../permissions';
import { makePermissionSelector } from '../../Selectors';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
const styles = (theme) => const styles = (theme) =>
@ -76,16 +78,21 @@ FileSharingModerator.propTypes =
classes : PropTypes.object.isRequired classes : PropTypes.object.isRequired
}; };
const makeMapStateToProps = () =>
{
const hasPermission = makePermissionSelector(permissions.MODERATE_FILES);
const mapStateToProps = (state) => const mapStateToProps = (state) =>
({ ({
isFileSharingModerator : isFileSharingModerator : hasPermission(state),
state.me.roles.some((role) =>
state.room.permissionsFromRoles.MODERATE_FILES.includes(role)),
room : state.room room : state.room
}); });
return mapStateToProps;
};
export default withRoomContext(connect( export default withRoomContext(connect(
mapStateToProps, makeMapStateToProps,
null, null,
null, null,
{ {
@ -93,7 +100,8 @@ export default withRoomContext(connect(
{ {
return ( return (
prev.room === next.room && prev.room === next.room &&
prev.me === next.me prev.me === next.me &&
prev.peers === next.peers
); );
} }
} }

View File

@ -1,8 +1,10 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
participantListSelector participantListSelector,
makePermissionSelector
} from '../../Selectors'; } from '../../Selectors';
import { permissions } from '../../../permissions';
import classnames from 'classnames'; import classnames from 'classnames';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../../RoomContext'; import { withRoomContext } from '../../../RoomContext';
@ -160,31 +162,34 @@ ParticipantList.propTypes =
classes : PropTypes.object.isRequired classes : PropTypes.object.isRequired
}; };
const makeMapStateToProps = () =>
{
const hasPermission = makePermissionSelector(permissions.MODERATE_ROOM);
const mapStateToProps = (state) => const mapStateToProps = (state) =>
{ {
return { return {
isModerator : isModerator : hasPermission(state),
state.me.roles.some((role) =>
state.room.permissionsFromRoles.MODERATE_ROOM.includes(role)),
participants : participantListSelector(state), participants : participantListSelector(state),
spotlights : state.room.spotlights, spotlights : state.room.spotlights,
selectedPeerId : state.room.selectedPeerId selectedPeerId : state.room.selectedPeerId
}; };
}; };
return mapStateToProps;
};
const ParticipantListContainer = withRoomContext(connect( const ParticipantListContainer = withRoomContext(connect(
mapStateToProps, makeMapStateToProps,
null, null,
null, null,
{ {
areStatesEqual : (next, prev) => areStatesEqual : (next, prev) =>
{ {
return ( return (
prev.room.permissionsFromRoles === next.room.permissionsFromRoles && prev.room === next.room &&
prev.me.roles === next.me.roles && prev.me.roles === next.me.roles &&
prev.peers === next.peers && prev.peers === next.peers
prev.room.spotlights === next.room.spotlights &&
prev.room.selectedPeerId === next.room.selectedPeerId
); );
} }
} }

View File

@ -1,5 +1,8 @@
import { createSelector } from 'reselect'; 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 producersSelect = (state) => state.producers;
const consumersSelect = (state) => state.consumers; const consumersSelect = (state) => state.consumers;
const spotlightsSelector = (state) => state.room.spotlights; 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;
}
);
};

View File

@ -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'
};

View File

@ -33,18 +33,8 @@ const initialState =
closeMeetingInProgress : false, closeMeetingInProgress : false,
clearChatInProgress : false, clearChatInProgress : false,
clearFileSharingInProgress : false, clearFileSharingInProgress : false,
userRoles : { NORMAL: 'normal' }, // Default role roomPermissions : null,
permissionsFromRoles : { allowWhenRoleMissing : null
CHANGE_ROOM_LOCK : [],
PROMOTE_PEER : [],
SEND_CHAT : [],
MODERATE_CHAT : [],
SHARE_SCREEN : [],
EXTRA_VIDEO : [],
SHARE_FILE : [],
MODERATE_FILES : [],
MODERATE_ROOM : []
}
}; };
const room = (state = initialState, action) => const room = (state = initialState, action) =>
@ -240,18 +230,18 @@ const room = (state = initialState, action) =>
case 'CLEAR_FILE_SHARING_IN_PROGRESS': case 'CLEAR_FILE_SHARING_IN_PROGRESS':
return { ...state, clearFileSharingInProgress: action.payload.flag }; 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: default:

11
server/access.js 100644
View File

@ -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'
};

View File

@ -1,5 +1,23 @@
const os = require('os'); const os = require('os');
const userRoles = require('../userRoles'); 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 AwaitQueue = require('awaitqueue');
// const axios = require('axios'); // const axios = require('axios');
@ -216,34 +234,40 @@ module.exports =
accessFromRoles : { accessFromRoles : {
// The role(s) will gain access to the room // The role(s) will gain access to the room
// even if it is locked (!) // 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 // The role(s) will gain access to the room without
// going into the lobby. If you want to restrict access to your // going into the lobby. If you want to restrict access to your
// server to only directly allow authenticated users, you could // server to only directly allow authenticated users, you could
// add the userRoles.AUTHENTICATED to the user in the userMapping // add the userRoles.AUTHENTICATED to the user in the userMapping
// function, and change to BYPASS_LOBBY : [ userRoles.AUTHENTICATED ] // function, and change to BYPASS_LOBBY : [ userRoles.AUTHENTICATED ]
BYPASS_LOBBY : [ userRoles.NORMAL ] [BYPASS_LOBBY] : [ userRoles.NORMAL ]
}, },
permissionsFromRoles : { permissionsFromRoles : {
// The role(s) have permission to lock/unlock a room // 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 // 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 // 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 // The role(s) have permission to moderate chat
MODERATE_CHAT : [ userRoles.MODERATOR ], [MODERATE_CHAT] : [ userRoles.MODERATOR ],
// The role(s) have permission to share screen // 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 // 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 // The role(s) have permission to share files
SHARE_FILE : [ userRoles.NORMAL ], [SHARE_FILE] : [ userRoles.NORMAL ],
// The role(s) have permission to moderate files // 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) // 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 // When truthy, the room will be open to all users when as long as there
// are allready users in the room // are allready users in the room
activateOnHostJoin : true, activateOnHostJoin : true,

View File

@ -5,29 +5,47 @@ const Lobby = require('./Lobby');
const { v4: uuidv4 } = require('uuid'); const { v4: uuidv4 } = require('uuid');
const jwt = require('jsonwebtoken'); const jwt = require('jsonwebtoken');
const userRoles = require('../userRoles'); 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 config = require('../config/config');
const logger = new Logger('Room'); const logger = new Logger('Room');
// In case they are not configured properly // In case they are not configured properly
const accessFromRoles = const roomAccess =
{ {
BYPASS_ROOM_LOCK : [ userRoles.ADMIN ], [BYPASS_ROOM_LOCK] : [ userRoles.ADMIN ],
BYPASS_LOBBY : [ userRoles.NORMAL ], [BYPASS_LOBBY] : [ userRoles.NORMAL ],
...config.accessFromRoles ...config.accessFromRoles
}; };
const permissionsFromRoles = const roomPermissions =
{ {
CHANGE_ROOM_LOCK : [ userRoles.NORMAL ], [CHANGE_ROOM_LOCK] : [ userRoles.NORMAL ],
PROMOTE_PEER : [ userRoles.NORMAL ], [PROMOTE_PEER] : [ userRoles.NORMAL ],
SEND_CHAT : [ userRoles.NORMAL ], [SEND_CHAT] : [ userRoles.NORMAL ],
MODERATE_CHAT : [ userRoles.MODERATOR ], [MODERATE_CHAT] : [ userRoles.MODERATOR ],
SHARE_SCREEN : [ userRoles.NORMAL ], [SHARE_SCREEN] : [ userRoles.NORMAL ],
EXTRA_VIDEO : [ userRoles.NORMAL ], [EXTRA_VIDEO] : [ userRoles.NORMAL ],
SHARE_FILE : [ userRoles.NORMAL ], [SHARE_FILE] : [ userRoles.NORMAL ],
MODERATE_FILES : [ userRoles.MODERATOR ], [MODERATE_FILES] : [ userRoles.MODERATOR ],
MODERATE_ROOM : [ userRoles.MODERATOR ], [MODERATE_ROOM] : [ userRoles.MODERATOR ],
...config.permissionsFromRoles ...config.permissionsFromRoles
}; };
@ -221,9 +239,8 @@ class Room extends EventEmitter
// Returning user // Returning user
if (returning) if (returning)
this._peerJoining(peer, true); this._peerJoining(peer, true);
else if ( // Has a role that is allowed to bypass room lock // Has a role that is allowed to bypass room lock
peer.roles.some((role) => accessFromRoles.BYPASS_ROOM_LOCK.includes(role)) else if (this._hasAccess(peer, BYPASS_ROOM_LOCK))
)
this._peerJoining(peer); this._peerJoining(peer);
else if ( else if (
'maxUsersPerRoom' in config && 'maxUsersPerRoom' in config &&
@ -239,7 +256,7 @@ class Room extends EventEmitter
else else
{ {
// Has a role that is allowed to bypass lobby // 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._peerJoining(peer) :
this._handleGuest(peer); this._handleGuest(peer);
} }
@ -271,11 +288,7 @@ class Room extends EventEmitter
this._peerJoining(promotedPeer); this._peerJoining(promotedPeer);
for ( for (const peer of this._getPeersWithPermission(PROMOTE_PEER))
const peer of this._getPeersWithPermission({
permission : permissionsFromRoles.PROMOTE_PEER
})
)
{ {
this._notification(peer.socket, 'lobby:promotedPeer', { peerId: id }); this._notification(peer.socket, 'lobby:promotedPeer', { peerId: id });
} }
@ -283,9 +296,8 @@ class Room extends EventEmitter
this._lobby.on('peerRolesChanged', (peer) => this._lobby.on('peerRolesChanged', (peer) =>
{ {
if ( // Has a role that is allowed to bypass room lock // Has a role that is allowed to bypass room lock
peer.roles.some((role) => accessFromRoles.BYPASS_ROOM_LOCK.includes(role)) if (this._hasAccess(peer, BYPASS_ROOM_LOCK))
)
{ {
this._lobby.promotePeer(peer.id); this._lobby.promotePeer(peer.id);
@ -294,7 +306,7 @@ class Room extends EventEmitter
if ( // Has a role that is allowed to bypass lobby if ( // Has a role that is allowed to bypass lobby
!this._locked && !this._locked &&
peer.roles.some((role) => accessFromRoles.BYPASS_LOBBY.includes(role)) this._hasAccess(peer, BYPASS_LOBBY)
) )
{ {
this._lobby.promotePeer(peer.id); this._lobby.promotePeer(peer.id);
@ -307,11 +319,7 @@ class Room extends EventEmitter
{ {
const { id, displayName } = changedPeer; const { id, displayName } = changedPeer;
for ( for (const peer of this._getPeersWithPermission(PROMOTE_PEER))
const peer of this._getPeersWithPermission({
permission : permissionsFromRoles.PROMOTE_PEER
})
)
{ {
this._notification(peer.socket, 'lobby:changeDisplayName', { peerId: id, displayName }); this._notification(peer.socket, 'lobby:changeDisplayName', { peerId: id, displayName });
} }
@ -321,11 +329,7 @@ class Room extends EventEmitter
{ {
const { id, picture } = changedPeer; const { id, picture } = changedPeer;
for ( for (const peer of this._getPeersWithPermission(PROMOTE_PEER))
const peer of this._getPeersWithPermission({
permission : permissionsFromRoles.PROMOTE_PEER
})
)
{ {
this._notification(peer.socket, 'lobby:changePicture', { peerId: id, picture }); this._notification(peer.socket, 'lobby:changePicture', { peerId: id, picture });
} }
@ -337,11 +341,7 @@ class Room extends EventEmitter
const { id } = closedPeer; const { id } = closedPeer;
for ( for (const peer of this._getPeersWithPermission(PROMOTE_PEER))
const peer of this._getPeersWithPermission({
permission : permissionsFromRoles.PROMOTE_PEER
})
)
{ {
this._notification(peer.socket, 'lobby:peerClosed', { peerId: id }); this._notification(peer.socket, 'lobby:peerClosed', { peerId: id });
} }
@ -401,7 +401,7 @@ class Room extends EventEmitter
); );
} }
async dump() dump()
{ {
return { return {
roomId : this._roomId, roomId : this._roomId,
@ -447,11 +447,7 @@ class Room extends EventEmitter
{ {
this._lobby.parkPeer(parkPeer); this._lobby.parkPeer(parkPeer);
for ( for (const peer of this._getPeersWithPermission(PROMOTE_PEER))
const peer of this._getPeersWithPermission({
permission : permissionsFromRoles.PROMOTE_PEER
})
)
{ {
this._notification(peer.socket, 'parkedPeer', { peerId: parkPeer.id }); this._notification(peer.socket, 'parkedPeer', { peerId: parkPeer.id });
} }
@ -602,7 +598,7 @@ class Room extends EventEmitter
// Got permission to promote peers, notify peer of // Got permission to promote peers, notify peer of
// peers in lobby // peers in lobby
if (permissionsFromRoles.PROMOTE_PEER.includes(newRole)) if (roomPermissions.PROMOTE_PEER.includes(newRole))
{ {
const lobbyPeers = this._lobby.peerList(); const lobbyPeers = this._lobby.peerList();
@ -671,11 +667,8 @@ class Room extends EventEmitter
let lobbyPeers = []; let lobbyPeers = [];
if ( // Allowed to promote peers, notify about lobbypeers // Allowed to promote peers, notify about lobbypeers
peer.roles.some((role) => if (this._hasPermission(peer, PROMOTE_PEER))
permissionsFromRoles.PROMOTE_PEER.includes(role)
)
)
lobbyPeers = this._lobby.peerList(); lobbyPeers = this._lobby.peerList();
cb(null, { cb(null, {
@ -683,8 +676,9 @@ class Room extends EventEmitter
peers : peerInfos, peers : peerInfos,
tracker : config.fileTracker, tracker : config.fileTracker,
authenticated : peer.authenticated, authenticated : peer.authenticated,
permissionsFromRoles : permissionsFromRoles, roomPermissions : roomPermissions,
userRoles : userRoles, userRoles : userRoles,
allowWhenRoleMissing : config.allowWhenRoleMissing,
chatHistory : this._chatHistory, chatHistory : this._chatHistory,
fileHistory : this._fileHistory, fileHistory : this._fileHistory,
lastNHistory : this._lastN, lastNHistory : this._lastN,
@ -711,7 +705,7 @@ class Room extends EventEmitter
} }
// Notify the new Peer to all other Peers. // 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( this._notification(
otherPeer.socket, otherPeer.socket,
@ -821,15 +815,13 @@ class Room extends EventEmitter
if ( if (
appData.source === 'screen' && appData.source === 'screen' &&
!peer.roles.some( !this._hasPermission(peer, SHARE_SCREEN)
(role) => permissionsFromRoles.SHARE_SCREEN.includes(role))
) )
throw new Error('peer not authorized'); throw new Error('peer not authorized');
if ( if (
appData.source === 'extravideo' && appData.source === 'extravideo' &&
!peer.roles.some( !this._hasPermission(peer, EXTRA_VIDEO)
(role) => permissionsFromRoles.EXTRA_VIDEO.includes(role))
) )
throw new Error('peer not authorized'); throw new Error('peer not authorized');
@ -882,7 +874,7 @@ class Room extends EventEmitter
cb(null, { id: producer.id }); cb(null, { id: producer.id });
// Optimization: Create a server-side Consumer for each Peer. // 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( this._createConsumer(
{ {
@ -1144,9 +1136,7 @@ class Room extends EventEmitter
case 'chatMessage': case 'chatMessage':
{ {
if ( if (!this._hasPermission(peer, SEND_CHAT))
!peer.roles.some((role) => permissionsFromRoles.SEND_CHAT.includes(role))
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
const { chatMessage } = request.data; const { chatMessage } = request.data;
@ -1167,11 +1157,7 @@ class Room extends EventEmitter
case 'moderator:clearChat': case 'moderator:clearChat':
{ {
if ( if (!this._hasPermission(peer, MODERATE_CHAT))
!peer.roles.some(
(role) => permissionsFromRoles.MODERATE_CHAT.includes(role)
)
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
this._chatHistory = []; this._chatHistory = [];
@ -1187,11 +1173,7 @@ class Room extends EventEmitter
case 'lockRoom': case 'lockRoom':
{ {
if ( if (!this._hasPermission(peer, CHANGE_ROOM_LOCK))
!peer.roles.some(
(role) => permissionsFromRoles.CHANGE_ROOM_LOCK.includes(role)
)
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
this._locked = true; this._locked = true;
@ -1209,11 +1191,7 @@ class Room extends EventEmitter
case 'unlockRoom': case 'unlockRoom':
{ {
if ( if (!this._hasPermission(peer, CHANGE_ROOM_LOCK))
!peer.roles.some(
(role) => permissionsFromRoles.CHANGE_ROOM_LOCK.includes(role)
)
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
this._locked = false; this._locked = false;
@ -1271,11 +1249,7 @@ class Room extends EventEmitter
case 'promotePeer': case 'promotePeer':
{ {
if ( if (!this._hasPermission(peer, PROMOTE_PEER))
!peer.roles.some(
(role) => permissionsFromRoles.PROMOTE_PEER.includes(role)
)
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
const { peerId } = request.data; const { peerId } = request.data;
@ -1290,11 +1264,7 @@ class Room extends EventEmitter
case 'promoteAllPeers': case 'promoteAllPeers':
{ {
if ( if (!this._hasPermission(peer, PROMOTE_PEER))
!peer.roles.some(
(role) => permissionsFromRoles.PROMOTE_PEER.includes(role)
)
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
this._lobby.promoteAllPeers(); this._lobby.promoteAllPeers();
@ -1307,11 +1277,7 @@ class Room extends EventEmitter
case 'sendFile': case 'sendFile':
{ {
if ( if (!this._hasPermission(peer, SHARE_FILE))
!peer.roles.some(
(role) => permissionsFromRoles.SHARE_FILE.includes(role)
)
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
const { magnetUri } = request.data; const { magnetUri } = request.data;
@ -1332,11 +1298,7 @@ class Room extends EventEmitter
case 'moderator:clearFileSharing': case 'moderator:clearFileSharing':
{ {
if ( if (!this._hasPermission(peer, MODERATE_FILES))
!peer.roles.some(
(role) => permissionsFromRoles.MODERATE_FILES.includes(role)
)
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
this._fileHistory = []; this._fileHistory = [];
@ -1371,11 +1333,7 @@ class Room extends EventEmitter
case 'moderator:mute': case 'moderator:mute':
{ {
if ( if (!this._hasPermission(peer, MODERATE_ROOM))
!peer.roles.some(
(role) => permissionsFromRoles.MODERATE_ROOM.includes(role)
)
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
const { peerId } = request.data; const { peerId } = request.data;
@ -1394,11 +1352,7 @@ class Room extends EventEmitter
case 'moderator:muteAll': case 'moderator:muteAll':
{ {
if ( if (!this._hasPermission(peer, MODERATE_ROOM))
!peer.roles.some(
(role) => permissionsFromRoles.MODERATE_ROOM.includes(role)
)
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
// Spread to others // Spread to others
@ -1411,11 +1365,7 @@ class Room extends EventEmitter
case 'moderator:stopVideo': case 'moderator:stopVideo':
{ {
if ( if (!this._hasPermission(peer, MODERATE_ROOM))
!peer.roles.some(
(role) => permissionsFromRoles.MODERATE_ROOM.includes(role)
)
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
const { peerId } = request.data; const { peerId } = request.data;
@ -1434,11 +1384,7 @@ class Room extends EventEmitter
case 'moderator:stopAllVideo': case 'moderator:stopAllVideo':
{ {
if ( if (!this._hasPermission(peer, MODERATE_ROOM))
!peer.roles.some(
(role) => permissionsFromRoles.MODERATE_ROOM.includes(role)
)
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
// Spread to others // Spread to others
@ -1451,11 +1397,7 @@ class Room extends EventEmitter
case 'moderator:closeMeeting': case 'moderator:closeMeeting':
{ {
if ( if (!this._hasPermission(peer, MODERATE_ROOM))
!peer.roles.some(
(role) => permissionsFromRoles.MODERATE_ROOM.includes(role)
)
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
this._notification(peer.socket, 'moderator:kick', null, true); this._notification(peer.socket, 'moderator:kick', null, true);
@ -1470,11 +1412,7 @@ class Room extends EventEmitter
case 'moderator:kickPeer': case 'moderator:kickPeer':
{ {
if ( if (!this._hasPermission(peer, MODERATE_ROOM))
!peer.roles.some(
(role) => permissionsFromRoles.MODERATE_ROOM.includes(role)
)
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
const { peerId } = request.data; const { peerId } = request.data;
@ -1495,11 +1433,7 @@ class Room extends EventEmitter
case 'moderator:lowerHand': case 'moderator:lowerHand':
{ {
if ( if (!this._hasPermission(peer, MODERATE_ROOM))
!peer.roles.some(
(role) => permissionsFromRoles.MODERATE_ROOM.includes(role)
)
)
throw new Error('peer not authorized'); throw new Error('peer not authorized');
const { peerId } = request.data; 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. * Helper to get the list of joined peers.
*/ */
_getJoinedPeers({ excludePeer = undefined } = {}) _getJoinedPeers(excludePeer = undefined)
{ {
return Object.values(this._peers) return Object.values(this._peers)
.filter((peer) => peer.joined && peer !== excludePeer); .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) return Object.values(this._peers)
.filter( .filter(
@ -1694,7 +1653,7 @@ class Room extends EventEmitter
peer.joined === joined && peer.joined === joined &&
peer !== excludePeer && peer !== excludePeer &&
peer.roles.some( peer.roles.some(
(role) => permission.includes(role) (role) => roomPermissions[permission].includes(role)
) )
); );
} }

View File

@ -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'
};