diff --git a/server/config/config.example.js b/server/config/config.example.js index 740a9ae..ee225b1 100644 --- a/server/config/config.example.js +++ b/server/config/config.example.js @@ -1,4 +1,5 @@ const os = require('os'); +const userRoles = require('../userRoles'); module.exports = { @@ -50,18 +51,95 @@ module.exports = // listeningRedirectPort disabled // use case: loadbalancer backend httpOnly : false, - // If this is set to true, only signed-in users will be able - // to join a room directly. Non-signed-in users (guests) will - // always be put in the lobby regardless of room lock status. - // If false, there is no difference between guests and signed-in - // users when joining. - requireSignInToAccess : true, - // This flag has no effect when requireSignInToAccess is false + // This function will be called on successful login through oidc. + // Use this function to map your oidc userinfo to the Peer object, + // see examples below. + // Examples: + /* + // All authenicated users will be MODERATOR and AUTHENTICATED + userMapping : async ({ peer, userinfo }) => + { + peer.addRole(userRoles.MODERATOR); + peer.addRole(userRoles.AUTHENTICATED); + }, + // All authenicated users will be AUTHENTICATED, + // and those with the moderator role set in the userinfo + // will also be MODERATOR + userMapping : async ({ peer, userinfo }) => + { + if ( + Array.isArray(userinfo.meet_roles) && + userinfo.meet_roles.includes('moderator') + ) + { + peer.addRole(userRoles.MODERATOR); + } + + if ( + Array.isArray(userinfo.meet_roles) && + userinfo.meet_roles.includes('meetingadmin') + ) + { + peer.addRole(userRoles.ADMIN); + } + + peer.addRole(userRoles.AUTHENTICATED); + }, + // All authenicated users will be AUTHENTICATED, + // and those with email ending with @example.com + // will also be MODERATOR + userMapping : async ({ peer, userinfo }) => + { + if (userinfo.email && userinfo.email.endsWith('@example.com')) + { + peer.addRole(userRoles.MODERATOR); + } + + peer.addRole(userRoles.AUTHENTICATED); + },*/ + userMapping : async ({ peer, userinfo }) => + { + if (userinfo.picture != null) + { + if (!userinfo.picture.match(/^http/g)) + { + peer.picture = `data:image/jpeg;base64, ${userinfo.picture}`; + } + else + { + peer.picture = userinfo.picture; + } + } + + if (userinfo.nickname != null) + { + peer.displayName = userinfo.nickname; + } + + if (userinfo.name != null) + { + peer.displayName = userinfo.name; + } + + if (userinfo.email != null) + { + peer.email = userinfo.email; + } + }, + // Required roles for Access. All users have the role "ALL" by default. + // Other roles need to be added in the "userMapping" function. This + // is an Array of roles. userRoles.ADMIN have all priveleges and access + // always. + // + // Example: + // [ userRoles.MODERATOR, userRoles.AUTHENTICATED ] + // This will allow all MODERATOR and AUTHENTICATED users access. + requiredRolesForAccess : [ userRoles.ALL ], // When truthy, the room will be open to all users when the first - // authenticated user has already joined the room. - activateOnHostJoin : true, + // AUTHENTICATED or MODERATOR user joins the room. + activateOnHostJoin : true, // Mediasoup settings - mediasoup : + mediasoup : { numWorkers : Object.keys(os.cpus()).length, // mediasoup Worker settings. diff --git a/server/lib/Lobby.js b/server/lib/Lobby.js index 75f4bf9..7372455 100644 --- a/server/lib/Lobby.js +++ b/server/lib/Lobby.js @@ -75,11 +75,15 @@ class Lobby extends EventEmitter if (peer) { peer.socket.removeListener('request', peer.socketRequestHandler); - peer.removeListener('authenticationChanged', peer.authenticationHandler); + peer.removeListener('rolesChange', peer.roleChangeHandler); + peer.removeListener('displayNameChanged', peer.displayNameChangeHandler); + peer.removeListener('pictureChanged', peer.pictureChangeHandler); peer.removeListener('close', peer.closeHandler); peer.socketRequestHandler = null; - peer.authenticationHandler = null; + peer.roleChangeHandler = null; + peer.displayNameChangeHandler = null; + peer.pictureChangeHandler = null; peer.closeHandler = null; this.emit('promotePeer', peer); @@ -112,16 +116,25 @@ class Lobby extends EventEmitter }); }; - peer.authenticationHandler = () => + peer.roleChangeHandler = () => { - logger.info('parkPeer() | authenticationChange [peer:"%s"]', peer.id); + logger.info('parkPeer() | rolesChange [peer:"%s"]', peer.id); - if (peer.authenticated) - { - this.emit('changeDisplayName', peer); - this.emit('changePicture', peer); - this.emit('peerAuthenticated', peer); - } + this.emit('peerRolesChanged', peer); + }; + + peer.displayNameChangeHandler = () => + { + logger.info('parkPeer() | displayNameChange [peer:"%s"]', peer.id); + + this.emit('changeDisplayName', peer); + }; + + peer.pictureChangeHandler = () => + { + logger.info('parkPeer() | pictureChange [peer:"%s"]', peer.id); + + this.emit('changePicture', peer); }; peer.closeHandler = () => @@ -143,7 +156,9 @@ class Lobby extends EventEmitter this._peers.set(peer.id, peer); - peer.on('authenticationChanged', peer.authenticationHandler); + peer.on('rolesChange', peer.roleChangeHandler); + peer.on('displayNameChanged', peer.displayNameChangeHandler); + peer.on('pictureChanged', peer.pictureChangeHandler); peer.socket.on('request', peer.socketRequestHandler); @@ -169,8 +184,6 @@ class Lobby extends EventEmitter peer.displayName = displayName; - this.emit('changeDisplayName', peer); - cb(); break; @@ -181,8 +194,6 @@ class Lobby extends EventEmitter peer.picture = picture; - this.emit('changePicture', peer); - cb(); break; diff --git a/server/lib/Peer.js b/server/lib/Peer.js index cce62aa..93ee9b8 100644 --- a/server/lib/Peer.js +++ b/server/lib/Peer.js @@ -1,4 +1,6 @@ const EventEmitter = require('events').EventEmitter; +const userRoles = require('../userRoles'); +const config = require('../config/config'); const Logger = require('./Logger'); const logger = new Logger('Peer'); @@ -22,7 +24,7 @@ class Peer extends EventEmitter this._inLobby = false; - this._authenticated = false; + this._roles = [ userRoles.ALL ]; this._displayName = false; @@ -40,8 +42,6 @@ class Peer extends EventEmitter this._consumers = new Map(); - this._checkAuthentication(); - this._handlePeer(); } @@ -66,13 +66,6 @@ class Peer extends EventEmitter _handlePeer() { - this.socket.use((packet, next) => - { - this._checkAuthentication(); - - return next(); - }); - this.socket.on('disconnect', () => { if (this.closed) @@ -84,33 +77,6 @@ class Peer extends EventEmitter }); } - _checkAuthentication() - { - if ( - Boolean(this.socket.handshake.session.passport) && - Boolean(this.socket.handshake.session.passport.user) - ) - { - const { - id, - displayName, - picture, - email - } = this.socket.handshake.session.passport.user; - - id && (this.authId = id); - displayName && (this.displayName = displayName); - picture && (this.picture = picture); - email && (this.email = email); - - this.authenticated = true; - } - else - { - this.authenticated = false; - } - } - get id() { return this._id; @@ -166,21 +132,9 @@ class Peer extends EventEmitter this._inLobby = inLobby; } - get authenticated() + get roles() { - return this._authenticated; - } - - set authenticated(authenticated) - { - if (authenticated !== this._authenticated) - { - const oldAuthenticated = this._authenticated; - - this._authenticated = authenticated; - - this.emit('authenticationChanged', { oldAuthenticated }); - } + return this._roles; } get displayName() @@ -262,6 +216,35 @@ class Peer extends EventEmitter return this._consumers; } + addRole(newRole) + { + if (!this._roles.includes(newRole)) + { + this._roles.push(newRole); + + logger.info('addRole() | [newRole:"%s]"', newRole); + + this.emit('rolesChange', { newRole }); + } + } + + removeRole(oldRole) + { + if (this._roles.includes(oldRole)) + { + this._roles = this._roles.filter((role) => role !== oldRole); + + logger.info('removeRole() | [oldRole:"%s]"', oldRole); + + this.emit('rolesChange', { oldRole }); + } + } + + hasRole(role) + { + return this._roles.includes(role); + } + addTransport(id, transport) { this.transports.set(id, transport); diff --git a/server/lib/Room.js b/server/lib/Room.js index f25f31d..140d4ee 100644 --- a/server/lib/Room.js +++ b/server/lib/Room.js @@ -1,6 +1,7 @@ const EventEmitter = require('events').EventEmitter; const Logger = require('./Logger'); const Lobby = require('./Lobby'); +const userRoles = require('../userRoles'); const config = require('../config/config'); const logger = new Logger('Room'); @@ -117,9 +118,8 @@ class Room extends EventEmitter handlePeer(peer) { - logger.info('handlePeer() [peer:"%s"]', peer.id); + logger.info('handlePeer() [peer:"%s", roles:"%s"]', peer.id, peer.roles); - // This will allow reconnects to join despite lock if (this._peers[peer.id]) { logger.warn( @@ -130,13 +130,16 @@ class Room extends EventEmitter return; } + + // Always let ADMIN in, even if locked + if (peer.roles.includes(userRoles.ADMIN)) + this._peerJoining(peer); else if (this._locked) - { this._parkPeer(peer); - } else { - peer.authenticated ? + // If the user has a role in config.requiredRolesForAccess, let them in + peer.roles.some((role) => config.requiredRolesForAccess.includes(role)) ? this._peerJoining(peer) : this._handleGuest(peer); } @@ -144,21 +147,12 @@ class Room extends EventEmitter _handleGuest(peer) { - if (config.requireSignInToAccess) - { - if (config.activateOnHostJoin && !this.checkEmpty()) - { - this._peerJoining(peer); - } - else - { - this._parkPeer(peer); - this._notification(peer.socket, 'signInRequired'); - } - } + if (config.activateOnHostJoin && !this.checkEmpty()) + this._peerJoining(peer); else { - this._peerJoining(peer); + this._parkPeer(peer); + this._notification(peer.socket, 'signInRequired'); } } @@ -178,9 +172,26 @@ class Room extends EventEmitter } }); - this._lobby.on('peerAuthenticated', (peer) => + this._lobby.on('peerRolesChanged', (peer) => { - !this._locked && this._lobby.promotePeer(peer.id); + // Always let admin in, even if locked + if (peer.roles.includes(userRoles.ADMIN)) + { + this._lobby.promotePeer(peer.id); + + return; + } + + // If the user has a role in config.requiredRolesForAccess, let them in + if ( + !this._locked && + peer.roles.some((role) => config.requiredRolesForAccess.includes(role)) + ) + { + this._lobby.promotePeer(peer.id); + + return; + } }); this._lobby.on('changeDisplayName', (changedPeer) => diff --git a/server/server.js b/server/server.js index a5d68a9..43b888b 100755 --- a/server/server.js +++ b/server/server.js @@ -241,51 +241,6 @@ function setupOIDC(oidcIssuer) _claims : tokenset.claims }; - if (userinfo.picture != null) - { - if (!userinfo.picture.match(/^http/g)) - { - user.picture = `data:image/jpeg;base64, ${userinfo.picture}`; - } - else - { - user.picture = userinfo.picture; - } - } - - if (userinfo.nickname != null) - { - user.displayName = userinfo.nickname; - } - - if (userinfo.name != null) - { - user.displayName = userinfo.name; - } - - if (userinfo.email != null) - { - user.email = userinfo.email; - } - - if (userinfo.given_name != null) - { - user.name={}; - user.name.givenName = userinfo.given_name; - } - - if (userinfo.family_name != null) - { - if (user.name == null) user.name={}; - user.name.familyName = userinfo.family_name; - } - - if (userinfo.middle_name != null) - { - if (user.name == null) user.name={}; - user.name.middleName = userinfo.middle_name; - } - return done(null, user); } ); @@ -349,7 +304,7 @@ async function setupAuth() app.get( '/auth/callback', passport.authenticate('oidc', { failureRedirect: '/auth/login' }), - (req, res) => + async (req, res) => { const state = JSON.parse(base64.decode(req.query.state)); @@ -373,7 +328,11 @@ async function setupAuth() peer && (peer.displayName = displayName); peer && (peer.picture = picture); - peer && (peer.authenticated = true); + + if (peer && typeof config.userMapping === 'function') + { + await config.userMapping({ peer, userinfo: req.user._userinfo }); + } res.send(loginHelper({ displayName, @@ -501,6 +460,30 @@ async function runWebSocketServer() peer.on('close', () => peers.delete(peerId)); + if ( + Boolean(socket.handshake.session.passport) && + Boolean(socket.handshake.session.passport.user) + ) + { + const { + id, + displayName, + picture, + email, + _userinfo + } = socket.handshake.session.passport.user; + + peer.authId= id; + peer.displayName = displayName; + peer.picture = picture; + peer.email = email; + + if (typeof config.userMapping === 'function') + { + await config.userMapping({ peer, userinfo: _userinfo }); + } + } + room.handlePeer(peer); }) .catch((error) =>