Add extra pad validation

Associate pads with meetings so session validation is restricted to the
meeting's valid session tokens.

Meteor will dispatch new redis events on shared notes and closed captions
pads creation. This event will go through apps and reach web to populate
a new meeting's pad collection that contains all valid pad id's for that
session. Nginx will use this collection to check if the user's session token
belongs to the pad's authorized users.

Besides these modifications, an extra change will be needed at notes.nginx.
Location /pad/p/ needs to change it's auth_request:

from /bigbluebutton/connection/checkAuthorization;
to /bigbluebutton/connection/validatePad;
This commit is contained in:
Pedro Beschorner Marin 2021-02-10 12:24:24 -03:00
parent c0a7f9cd92
commit 09b39a8d63
23 changed files with 310 additions and 4 deletions

View File

@ -76,6 +76,10 @@ class ReceivedJsonMsgHandlerActor(
routeGenericMsg[EjectUserFromMeetingSysMsg](envelope, jsonNode)
case ValidateConnAuthTokenSysMsg.NAME =>
route[ValidateConnAuthTokenSysMsg](meetingManagerChannel, envelope, jsonNode)
case AddPadSysMsg.NAME =>
routeGenericMsg[AddPadSysMsg](envelope, jsonNode)
case AddCaptionsPadsSysMsg.NAME =>
routeGenericMsg[AddCaptionsPadsSysMsg](envelope, jsonNode)
// Guests
case GetGuestsWaitingApprovalReqMsg.NAME =>

View File

@ -26,6 +26,7 @@ import org.bigbluebutton.core.models._
import org.bigbluebutton.core2.{ MeetingStatus2x, Permissions }
import org.bigbluebutton.core2.message.handlers._
import org.bigbluebutton.core2.message.handlers.meeting._
import org.bigbluebutton.core2.message.handlers.pads._
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.apps.breakout._
import org.bigbluebutton.core.apps.polls._
@ -84,6 +85,8 @@ class MeetingActor(
with SyncGetMeetingInfoRespMsgHdlr
with ClientToServerLatencyTracerMsgHdlr
with ValidateConnAuthTokenSysMsgHdlr
with AddPadSysMsgHdlr
with AddCaptionsPadsSysMsgHdlr
with UserActivitySignCmdMsgHdlr {
object CheckVoiceRecordingInternalMsg
@ -491,6 +494,9 @@ class MeetingActor(
case m: ValidateConnAuthTokenSysMsg => handleValidateConnAuthTokenSysMsg(m)
case m: AddPadSysMsg => handleAddPadSysMsg(m)
case m: AddCaptionsPadsSysMsg => handleAddCaptionsPadsSysMsg(m)
case m: UserActivitySignCmdMsg => handleUserActivitySignCmdMsg(m)
case _ => log.warning("***** Cannot handle " + msg.envelope.name)

View File

@ -0,0 +1,21 @@
package org.bigbluebutton.core2.message.handlers.pads
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.running.{ BaseMeetingActor, LiveMeeting, OutMsgRouter }
import org.bigbluebutton.core2.message.senders.MsgBuilder
trait AddCaptionsPadsSysMsgHdlr {
this: BaseMeetingActor =>
val liveMeeting: LiveMeeting
val outGW: OutMsgRouter
def handleAddCaptionsPadsSysMsg(msg: AddCaptionsPadsSysMsg) {
val padIds = msg.body.padIds
val meetingId = liveMeeting.props.meetingProp.intId
log.info(s"Handling add captions pads for meetingId=${meetingId}")
outGW.send(MsgBuilder.buildAddCaptionsPadsEvtMsg(meetingId, padIds))
}
}

View File

@ -0,0 +1,22 @@
package org.bigbluebutton.core2.message.handlers.pads
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.running.{ BaseMeetingActor, LiveMeeting, OutMsgRouter }
import org.bigbluebutton.core2.message.senders.MsgBuilder
trait AddPadSysMsgHdlr {
this: BaseMeetingActor =>
val liveMeeting: LiveMeeting
val outGW: OutMsgRouter
def handleAddPadSysMsg(msg: AddPadSysMsg) {
val padId = msg.body.padId
val readOnlyId = msg.body.readOnlyId
val meetingId = liveMeeting.props.meetingProp.intId
log.info(s"Handling add padId=${padId} and readOnlyId=${readOnlyId} for meetingId=${meetingId}")
outGW.send(MsgBuilder.buildAddPadEvtMsg(meetingId, padId, readOnlyId))
}
}

View File

@ -83,6 +83,26 @@ object MsgBuilder {
BbbCommonEnvCoreMsg(envelope, event)
}
def buildAddPadEvtMsg(meetingId: String, padId: String, readOnlyId: String): BbbCommonEnvCoreMsg = {
val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
val envelope = BbbCoreEnvelope(AddPadEvtMsg.NAME, routing)
val header = BbbCoreHeaderWithMeetingId(AddPadEvtMsg.NAME, meetingId)
val body = AddPadEvtMsgBody(padId, readOnlyId)
val event = AddPadEvtMsg(header, body)
BbbCommonEnvCoreMsg(envelope, event)
}
def buildAddCaptionsPadsEvtMsg(meetingId: String, padIds: Array[String]): BbbCommonEnvCoreMsg = {
val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
val envelope = BbbCoreEnvelope(AddCaptionsPadsEvtMsg.NAME, routing)
val header = BbbCoreHeaderWithMeetingId(AddCaptionsPadsEvtMsg.NAME, meetingId)
val body = AddCaptionsPadsEvtMsgBody(padIds)
val event = AddCaptionsPadsEvtMsg(header, body)
BbbCommonEnvCoreMsg(envelope, event)
}
def buildGetUsersMeetingRespMsg(meetingId: String, userId: String, webusers: Vector[WebUser]): BbbCommonEnvCoreMsg = {
val routing = Routing.addMsgToClientRouting(MessageTypes.DIRECT, meetingId, userId)
val envelope = BbbCoreEnvelope(GetUsersMeetingRespMsg.NAME, routing)

View File

@ -189,6 +189,22 @@ case class ValidateConnAuthTokenSysRespMsg(
case class ValidateConnAuthTokenSysRespMsgBody(meetingId: String, userId: String,
connId: String, authzed: Boolean, app: String)
object AddPadSysMsg { val NAME = "AddPadSysMsg" }
case class AddPadSysMsg(header: BbbClientMsgHeader, body: AddPadSysMsgBody) extends StandardMsg
case class AddPadSysMsgBody(padId: String, readOnlyId: String)
object AddCaptionsPadsSysMsg { val NAME = "AddCaptionsPadsSysMsg" }
case class AddCaptionsPadsSysMsg(header: BbbClientMsgHeader, body: AddCaptionsPadsSysMsgBody) extends StandardMsg
case class AddCaptionsPadsSysMsgBody(padIds: Array[String])
object AddPadEvtMsg { val NAME = "AddPadEvtMsg" }
case class AddPadEvtMsg(header: BbbCoreHeaderWithMeetingId, body: AddPadEvtMsgBody) extends BbbCoreMsg
case class AddPadEvtMsgBody(padId: String, readOnlyId: String)
object AddCaptionsPadsEvtMsg { val NAME = "AddCaptionsPadsEvtMsg" }
case class AddCaptionsPadsEvtMsg(header: BbbCoreHeaderWithMeetingId, body: AddCaptionsPadsEvtMsgBody) extends BbbCoreMsg
case class AddCaptionsPadsEvtMsgBody(padIds: Array[String])
object PublishedRecordingSysMsg { val NAME = "PublishedRecordingSysMsg" }
case class PublishedRecordingSysMsg(header: BbbCoreBaseHeader, body: PublishedRecordingSysMsgBody) extends BbbCoreMsg
case class PublishedRecordingSysMsgBody(recordId: String)

View File

@ -54,6 +54,8 @@ import org.bigbluebutton.api.messaging.converters.messages.EndMeetingMessage;
import org.bigbluebutton.api.messaging.converters.messages.PublishedRecordingMessage;
import org.bigbluebutton.api.messaging.converters.messages.UnpublishedRecordingMessage;
import org.bigbluebutton.api.messaging.converters.messages.DeletedRecordingMessage;
import org.bigbluebutton.api.messaging.messages.AddPad;
import org.bigbluebutton.api.messaging.messages.AddCaptionsPads;
import org.bigbluebutton.api.messaging.messages.CreateBreakoutRoom;
import org.bigbluebutton.api.messaging.messages.CreateMeeting;
import org.bigbluebutton.api.messaging.messages.EndMeeting;
@ -161,6 +163,20 @@ public class MeetingService implements MessageListener {
}
}
public Boolean isPadValid(String padId, String sessionToken) {
UserSession us = getUserSessionWithAuthToken(sessionToken);
if (us == null) return false;
Meeting m = getMeeting(us.meetingID);
if (m == null) return false;
if (m.hasPad(padId)) {
return true;
} else {
return false;
}
}
public UserSession getUserSessionWithUserId(String userId) {
for (UserSession userSession : sessions.values()) {
if (userSession.internalUserId.equals(userId)) {
@ -1051,6 +1067,10 @@ public class MeetingService implements MessageListener {
processGuestPolicyChanged((GuestPolicyChanged) message);
} else if (message instanceof RecordChapterBreak) {
processRecordingChapterBreak((RecordChapterBreak) message);
} else if (message instanceof AddPad) {
processAddPad((AddPad) message);
} else if (message instanceof AddCaptionsPads) {
processAddCaptionsPads((AddCaptionsPads) message);
} else if (message instanceof MakePresentationDownloadableMsg) {
processMakePresentationDownloadableMsg((MakePresentationDownloadableMsg) message);
} else if (message instanceof UpdateRecordingStatus) {
@ -1069,6 +1089,22 @@ public class MeetingService implements MessageListener {
}
}
public void processAddPad(AddPad msg) {
Meeting m = getMeeting(msg.meetingId);
if (m != null) {
m.addPad(msg.padId, msg.readOnlyId);
}
}
public void processAddCaptionsPads(AddCaptionsPads msg) {
Meeting m = getMeeting(msg.meetingId);
if (m != null) {
for (String padId : msg.padIds) {
m.addPad(padId, "undefined");
}
}
}
public void processRecordingChapterBreak(RecordChapterBreak msg) {
recordingService.kickOffRecordingChapterBreak(msg.meetingId, msg.timestamp);
}

View File

@ -69,6 +69,7 @@ public class Meeting {
private String defaultConfigToken;
private String guestPolicy = GuestPolicy.ASK_MODERATOR;
private boolean userHasJoined = false;
private Map<String, String> pads;
private Map<String, String> metadata;
private Map<String, Object> userCustomData;
private final ConcurrentMap<String, User> users;
@ -131,6 +132,13 @@ public class Meeting {
allowDuplicateExtUserid = builder.allowDuplicateExtUserid;
endWhenNoModerator = builder.endWhenNoModerator;
/*
* A pad is a pair of padId and readOnlyId that represents
* valid etherpads instances for this meeting. They can be:
* - shared notes
* - closed captions
*/
pads = new HashMap<>();
userCustomData = new HashMap<>();
users = new ConcurrentHashMap<>();
@ -179,6 +187,10 @@ public class Meeting {
return configs.remove(token);
}
public Map<String, String> getPads() {
return pads;
}
public Map<String, String> getMetadata() {
return metadata;
}
@ -537,6 +549,14 @@ public class Meeting {
return sum;
}
public void addPad(String padId, String readOnlyId) {
pads.put(padId, readOnlyId);
}
public Boolean hasPad(String id) {
return pads.containsKey(id) || pads.containsValue(id);
}
public void addUserCustomData(String userID, Map<String, String> data) {
userCustomData.put(userID, data);
}

View File

@ -0,0 +1,11 @@
package org.bigbluebutton.api.messaging.messages;
public class AddCaptionsPads implements IMessage {
public final String meetingId;
public final String[] padIds;
public AddCaptionsPads(String meetingId, String[] padIds) {
this.meetingId = meetingId;
this.padIds = padIds;
}
}

View File

@ -0,0 +1,13 @@
package org.bigbluebutton.api.messaging.messages;
public class AddPad implements IMessage {
public final String meetingId;
public final String padId;
public final String readOnlyId;
public AddPad(String meetingId, String padId, String readOnlyId) {
this.meetingId = meetingId;
this.padId = padId;
this.readOnlyId = readOnlyId;
}
}

View File

@ -51,4 +51,20 @@ public class ParamsUtil {
}
return token;
}
public static String getPadId(String url) {
String padId = "undefined";
try {
String decodedURL = URLDecoder.decode(url, "UTF-8");
String[] splitURL = decodedURL.split("\\?");
// If there is no query params, it's an invalid URL already
if (splitURL.length == 2) {
String[] params = splitURL[0].split("\\/");
padId = params[params.length - 1];
}
} catch (UnsupportedEncodingException e) {
log.error(e.toString());
}
return padId;
}
}

View File

@ -94,6 +94,10 @@ class ReceivedJsonMsgHdlrActor(val msgFromAkkaAppsEventBus: MsgFromAkkaAppsEvent
route[GuestsWaitingApprovedEvtMsg](envelope, jsonNode)
case GuestPolicyChangedEvtMsg.NAME =>
route[GuestPolicyChangedEvtMsg](envelope, jsonNode)
case AddPadEvtMsg.NAME =>
route[AddPadEvtMsg](envelope, jsonNode)
case AddCaptionsPadsEvtMsg.NAME =>
route[AddCaptionsPadsEvtMsg](envelope, jsonNode)
case RecordingChapterBreakSysMsg.NAME =>
route[RecordingChapterBreakSysMsg](envelope, jsonNode)
case SetPresentationDownloadableEvtMsg.NAME =>

View File

@ -39,6 +39,8 @@ class OldMeetingMsgHdlrActor(val olgMsgGW: OldMessageReceivedGW)
case m: PresentationUploadTokenSysPubMsg => handlePresentationUploadTokenSysPubMsg(m)
case m: GuestsWaitingApprovedEvtMsg => handleGuestsWaitingApprovedEvtMsg(m)
case m: GuestPolicyChangedEvtMsg => handleGuestPolicyChangedEvtMsg(m)
case m: AddCaptionsPadsEvtMsg => handleAddCaptionsPadsEvtMsg(m)
case m: AddPadEvtMsg => handleAddPadEvtMsg(m)
case m: RecordingChapterBreakSysMsg => handleRecordingChapterBreakSysMsg(m)
case m: SetPresentationDownloadableEvtMsg => handleSetPresentationDownloadableEvtMsg(m)
case m: RecordingStatusChangedEvtMsg => handleRecordingStatusChangedEvtMsg(m)
@ -50,6 +52,14 @@ class OldMeetingMsgHdlrActor(val olgMsgGW: OldMessageReceivedGW)
olgMsgGW.handle(new GuestPolicyChanged(msg.header.meetingId, msg.body.policy))
}
def handleAddPadEvtMsg(msg: AddPadEvtMsg): Unit = {
olgMsgGW.handle(new AddPad(msg.header.meetingId, msg.body.padId, msg.body.readOnlyId))
}
def handleAddCaptionsPadsEvtMsg(msg: AddCaptionsPadsEvtMsg): Unit = {
olgMsgGW.handle(new AddCaptionsPads(msg.header.meetingId, msg.body.padIds))
}
def handleRecordingChapterBreakSysMsg(msg: RecordingChapterBreakSysMsg): Unit = {
olgMsgGW.handle(new RecordChapterBreak(msg.body.meetingId, msg.body.timestamp))
}

View File

@ -0,0 +1,19 @@
import RedisPubSub from '/imports/startup/server/redis';
import Logger from '/imports/startup/server/logger';
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
export default function addCaptionsPads(meetingId, padIds) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'AddCaptionsPadsSysMsg';
check(meetingId, String);
check(padIds, [String]);
const payload = {
padIds,
};
return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, 'nodeJSapp', payload);
}

View File

@ -0,0 +1,32 @@
import RedisPubSub from '/imports/startup/server/redis';
import Captions from '/imports/api/captions';
import Logger from '/imports/startup/server/logger';
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
export default function addPad(padId, readOnlyId) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'AddPadSysMsg';
check(padId, String);
check(readOnlyId, String);
const pad = Captions.findOne({ padId });
if (!pad) {
Logger.error(`Could not find closed captions pad ${padId}`);
return;
}
const { meetingId } = pad;
check(meetingId, String);
const payload = {
padId,
readOnlyId,
};
return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, 'nodeJSapp', payload);
}

View File

@ -6,6 +6,7 @@ import {
getLocalesURL,
} from '/imports/api/captions/server/helpers';
import addCaption from '/imports/api/captions/server/modifiers/addCaption';
import addCaptionsPads from '/imports/api/captions/server/methods/addCaptionsPads';
import axios from 'axios';
export default function createCaptions(meetingId) {
@ -27,10 +28,13 @@ export default function createCaptions(meetingId) {
Logger.error(`Could not get locales info for ${meetingId} ${status}`);
return;
}
const padIds = [];
const locales = response.data;
locales.forEach((locale) => {
const padId = generatePadId(meetingId, locale.locale);
addCaption(meetingId, padId, locale);
padIds.push(padId);
});
addCaptionsPads(meetingId, padIds);
}).catch(error => Logger.error(`Could not create captions for ${meetingId}: ${error}`));
}

View File

@ -1,6 +1,7 @@
import Captions from '/imports/api/captions';
import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check';
import addPad from '/imports/api/captions/server/methods/addPad';
export default function updateReadOnlyPadId(padId, readOnlyPadId) {
check(padId, String);
@ -20,6 +21,7 @@ export default function updateReadOnlyPadId(padId, readOnlyPadId) {
const numberAffected = Captions.update(selector, modifier, { multi: true });
if (numberAffected) {
addPad(padId, readOnlyPadId);
Logger.verbose('Captions: added readOnlyPadId', { padId, readOnlyPadId });
}
} catch (err) {

View File

@ -0,0 +1,21 @@
import RedisPubSub from '/imports/startup/server/redis';
import Logger from '/imports/startup/server/logger';
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
export default function addPad(meetingId, padId, readOnlyId) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'AddPadSysMsg';
check(meetingId, String);
check(padId, String);
check(readOnlyId, String);
const payload = {
padId,
readOnlyId,
};
return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, 'nodeJSapp', payload);
}

View File

@ -1,6 +1,7 @@
import { check } from 'meteor/check';
import Note from '/imports/api/note';
import Logger from '/imports/startup/server/logger';
import addPad from '/imports/api/note/server/methods/addPad';
export default function addNote(meetingId, noteId, readOnlyNoteId) {
check(meetingId, String);
@ -23,6 +24,7 @@ export default function addNote(meetingId, noteId, readOnlyNoteId) {
const { insertedId } = Note.upsert(selector, modifier);
if (insertedId) {
addPad(meetingId, noteId, readOnlyNoteId);
Logger.info(`Added note id=${noteId} readOnlyId=${readOnlyNoteId} meeting=${meetingId}`);
} else {
Logger.info(`Upserted note id=${noteId} readOnlyId=${readOnlyNoteId} meeting=${meetingId}`);

View File

@ -25,6 +25,8 @@ const getLang = () => {
const getNoteParams = () => {
let config = {};
const User = Users.findOne({ userId: Auth.userID }, { fields: { name: 1 } });
config.userName = User.name;
config.lang = getLang();
config.rtl = document.documentElement.getAttribute('dir') === 'rtl';

View File

@ -88,6 +88,14 @@
proxy_set_header X-Original-URI $request_uri;
}
location = /bigbluebutton/connection/validatePad {
internal;
proxy_pass http://127.0.0.1:8090;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Original-URI $request_uri;
}
location ~ "^/bigbluebutton\/textTrack\/(?<textTrackToken>[a-zA-Z0-9]+)\/(?<recordId>[a-zA-Z0-9_-]+)\/(?<textTrack>.+)$" {
# Workaround IE refusal to set cookies in iframe
add_header P3P 'CP="No P3P policy available"';

View File

@ -95,10 +95,6 @@ class UrlMappings {
action = [POST: 'putRecordingTextTrack']
}
"/connection/checkAuthorization"(controller:"connection") {
action = [GET:'checkAuthorization']
}
"/bigbluebutton/$controller/$action?/$id?(.${format})?" {
constraints {
// apply constraints here

View File

@ -44,4 +44,25 @@ class ConnectionController {
log.error("Error while authenticating connection.\n" + e.getMessage())
}
}
def validatePad = {
try {
String uri = request.getHeader("x-original-uri")
String sessionToken = ParamsUtil.getSessionToken(uri)
String padId = ParamsUtil.getPadId(uri)
Boolean valid = meetingService.isPadValid(padId, sessionToken)
response.addHeader("Cache-Control", "no-cache")
response.contentType = 'plain/text'
if (valid) {
response.setStatus(200)
response.outputStream << 'authorized'
} else {
response.setStatus(401)
response.outputStream << 'unauthorized'
}
} catch (IOException e) {
log.error("Error while authenticating connection.\n" + e.getMessage())
}
}
}