feature: Override client settings through API /create call (#18782)

* akka-with-client-configs

* [akka-with-client-configs] -  inserted client configs in akka

* [issue-18588-create-override] - WIP

* [akka-with-client-configs] - Remove unnecessary code

* [issue-18588] - test some thesis

* [akka-with-client-configs] - refactor to add jackson and immutable.Map

* [issue-18588-create-override] - new architecture for overriding client configs]

* [issue-18588-create-override] - implemented settings

* Refactor on clientSettingsOverride module and add allowOverrideClientSettingsOnCreateCall conf

---------

Co-authored-by: Gustavo Trott <gustavo@trott.com.br>
This commit is contained in:
Guilherme Pereira Leme 2023-10-10 21:00:20 -03:00 committed by GitHub
parent ab3c57b858
commit 4ef078ccf5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 135 additions and 64 deletions

View File

@ -37,6 +37,7 @@ object Dependencies {
val scalaTest = "3.2.11"
val mockito = "2.23.0"
val akkaTestKit = "2.6.0"
val jacksonDataFormat = "2.13.5"
}
object Compile {
@ -65,6 +66,8 @@ object Dependencies {
val slickHikaricp = "com.typesafe.slick" %% "slick-hikaricp" % Versions.slick
val slickPg = "com.github.tminglei" %% "slick-pg" % Versions.slickPg
val postgresql = "org.postgresql" % "postgresql" % Versions.postgresql
val jacksonDataFormat = "com.fasterxml.jackson.dataformat" % "jackson-dataformat-yaml" % Versions.jacksonDataFormat
val snakeYaml = "org.yaml" % "snakeyaml"
}
object Test {
@ -101,5 +104,6 @@ object Dependencies {
Compile.slick,
Compile.slickHikaricp,
Compile.slickPg,
Compile.postgresql) ++ testing
Compile.postgresql,
Compile.jacksonDataFormat) ++ testing
}

View File

@ -1,23 +1,17 @@
package org.bigbluebutton
import com.typesafe.config.ConfigFactory
import org.bigbluebutton.common2.util.YamlUtil
import org.slf4j.LoggerFactory
import java.io.{ ByteArrayInputStream, File }
import scala.io.BufferedSource
import scala.util.{ Failure, Success, Try }
object ClientSettings {
val config = ConfigFactory.load()
object ClientSettings extends SystemConfiguration {
var clientSettingsFromFile: Map[String, Object] = Map("" -> "")
val logger = LoggerFactory.getLogger(this.getClass)
def loadClientSettingsFromFile() = {
lazy val clientSettingsPath = Try(config.getString("client.clientSettingsFilePath")).getOrElse(
"/usr/share/meteor/bundle/programs/server/assets/app/config/settings.yml"
)
lazy val clientSettingsPathOverride = Try(config.getString("client.clientSettingsOverrideFilePath")).getOrElse(
"/etc/bigbluebutton/bbb-html5.yml"
)
val clientSettingsFile = scala.io.Source.fromFile(clientSettingsPath, "UTF-8")
val clientSettingsFileOverrideToCheck = new File(clientSettingsPathOverride)
@ -45,4 +39,17 @@ object ClientSettings {
}
)
}
def getClientSettingsWithOverride(clientSettingsOverrideJson: String): Map[String, Object] = {
if (clientSettingsOverrideJson.nonEmpty) {
val scalaMapClientOverride = common2.util.JsonUtil.toMap[Object](clientSettingsOverrideJson)
scalaMapClientOverride match {
case Success(clientSettingsOverrideAsMap) => YamlUtil.mergeImmutableMaps(clientSettingsFromFile, clientSettingsOverrideAsMap)
case Failure(msg) =>
logger.debug("No valid JSON override of client configuration in create call: {}", msg)
clientSettingsFromFile
}
} else clientSettingsFromFile
}
}

View File

@ -1,6 +1,9 @@
package org.bigbluebutton
import scala.util.Try
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
import scala.util.{ Failure, Success, Try }
import com.typesafe.config.ConfigFactory
trait SystemConfiguration {
@ -77,6 +80,13 @@ trait SystemConfiguration {
lazy val analyticsIncludeChat = Try(config.getBoolean("analytics.includeChat")).getOrElse(true)
lazy val clientSettingsPath = Try(config.getString("client.clientSettingsFilePath")).getOrElse(
"/usr/share/meteor/bundle/programs/server/assets/app/config/settings.yml"
)
lazy val clientSettingsPathOverride = Try(config.getString("client.clientSettingsOverrideFilePath")).getOrElse(
"/etc/bigbluebutton/bbb-html5.yml"
)
// Grab the "interface" parameter from the http config
val httpHost = config.getString("http.interface")
// Grab the "port" parameter from the http config

View File

@ -19,12 +19,6 @@ import org.bigbluebutton.core.util.ColorPicker
import org.bigbluebutton.core2.RunningMeetings
import org.bigbluebutton.core2.message.senders.MsgBuilder
import org.bigbluebutton.service.HealthzService
import org.bigbluebutton.common2
import org.bigbluebutton.common2.util.YamlUtil
import scala.jdk.CollectionConverters._
import java.util
import scala.util.{ Failure, Success }
object BigBlueButtonActor extends SystemConfiguration {
def props(

View File

@ -34,6 +34,7 @@ object JsonUtils {
case m: Map[_, _] => JsObject(m.asInstanceOf[Map[String, Any]].map { case (k, v) => k -> write(v) })
case l: List[_] => JsArray(l.map(write).toVector)
case a: Array[_] => JsArray(a.map(write).toVector)
case null => JsNull
case _ => throw new IllegalArgumentException(s"Unsupported type: ${x.getClass.getName}")
// case _ => JsNull
}

View File

@ -35,7 +35,7 @@ class RunningMeeting(val props: DefaultProps, outGW: OutMessageGateway,
private val deskshareModel = new ScreenshareModel
private val audioCaptions = new AudioCaptions
private val timerModel = new TimerModel
private val clientSettings = ClientSettings.clientSettingsFromFile
private val clientSettings: Map[String, Object] = ClientSettings.getClientSettingsWithOverride(props.overrideClientSettings)
// meetingModel.setGuestPolicy(props.usersProp.guestPolicy)

View File

@ -89,7 +89,8 @@ case class DefaultProps(
metadataProp: MetadataProp,
lockSettingsProps: LockSettingsProps,
systemProps: SystemProps,
groups: Vector[GroupProps]
groups: Vector[GroupProps],
overrideClientSettings: String
)
case class StartEndTimeStatus(startTime: Long, endTime: Long)

View File

@ -407,7 +407,8 @@ public class MeetingService implements MessageListener {
m.getMuteOnStart(), m.getAllowModsToUnmuteUsers(), m.getAllowModsToEjectCameras(), m.getMeetingKeepEvents(),
m.breakoutRoomsParams, m.lockSettingsParams, m.getHtml5InstanceId(),
m.getGroups(), m.getDisabledFeatures(), m.getNotifyRecordingIsOn(),
m.getPresentationUploadExternalDescription(), m.getPresentationUploadExternalUrl());
m.getPresentationUploadExternalDescription(), m.getPresentationUploadExternalUrl(),
m.getOverrideClientSettings());
}
private String formatPrettyDate(Long timestamp) {

View File

@ -19,13 +19,10 @@
package org.bigbluebutton.api;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -37,12 +34,6 @@ import com.google.gson.JsonObject;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.util.EntityUtils;
import org.bigbluebutton.api.domain.BreakoutRoomsParams;
import org.bigbluebutton.api.domain.LockSettingsParams;
import org.bigbluebutton.api.domain.Meeting;
@ -145,6 +136,7 @@ public class ParamsProcessorUtil {
private String bbbVersion = "";
private Boolean allowRevealOfBBBVersion = false;
private Boolean allowOverrideClientSettingsOnCreateCall = false;
private String formatConfNum(String s) {
if (s.length() > 5) {
@ -912,6 +904,10 @@ public class ParamsProcessorUtil {
return allowRevealOfBBBVersion;
}
public Boolean getAllowOverrideClientSettingsOnCreateCall() {
return allowOverrideClientSettingsOnCreateCall;
}
public String processWelcomeMessage(String message, Boolean isBreakout) {
String welcomeMessage = message;
if (StringUtils.isEmpty(message)) {
@ -1515,4 +1511,8 @@ public class ParamsProcessorUtil {
this.allowRevealOfBBBVersion = allowVersion;
}
public void setAllowOverrideClientSettingsOnCreateCall(Boolean allowOverrideClientSettingsOnCreateCall) {
this.allowOverrideClientSettingsOnCreateCall = allowOverrideClientSettingsOnCreateCall;
}
}

View File

@ -118,6 +118,8 @@ public class Meeting {
private Integer html5InstanceId;
private String overrideClientSettings = "";
public Meeting(Meeting.Builder builder) {
name = builder.name;
extMeetingId = builder.externalId;
@ -1131,4 +1133,12 @@ public class Meeting {
return new Meeting(this);
}
}
public String getOverrideClientSettings() {
return overrideClientSettings;
}
public void setOverrideClientSettings(String overrideClientConfigs) {
this.overrideClientSettings = overrideClientConfigs;
}
}

View File

@ -46,7 +46,8 @@ public interface IBbbWebApiGWApp {
ArrayList<String> disabledFeatures,
Boolean notifyRecordingIsOn,
String presentationUploadExternalDescription,
String presentationUploadExternalUrl);
String presentationUploadExternalUrl,
String overrideClientSettings);
void registerUser(String meetingID, String internalUserId, String fullname, String role,
String externUserID, String authToken, String sessionToken, String avatarURL,

View File

@ -153,7 +153,8 @@ class BbbWebApiGWApp(
disabledFeatures: java.util.ArrayList[String],
notifyRecordingIsOn: java.lang.Boolean,
presentationUploadExternalDescription: String,
presentationUploadExternalUrl: String): Unit = {
presentationUploadExternalUrl: String,
overrideClientSettings: String): Unit = {
val disabledFeaturesAsVector: Vector[String] = disabledFeatures.asScala.toVector
@ -246,7 +247,8 @@ class BbbWebApiGWApp(
metadataProp,
lockSettingsProps,
systemProps,
groupsAsVector
groupsAsVector,
overrideClientSettings
)
//meetingManagerActorRef ! new CreateMeetingMsg(defaultProps)

View File

@ -155,6 +155,7 @@ export default async function addMeeting(meeting) {
html5InstanceId: Number,
},
groups: Array,
overrideClientSettings: String,
});
const {

View File

@ -348,6 +348,9 @@ allowFetchAllRecordings=true
# The directory where the pre-built configs are stored
configDir=/var/bigbluebutton/configs
# Disable this option to avoid overriding client settings through /create call
allowOverrideClientSettingsOnCreateCall=true
# The directory to export Json with Meeting activities (used in Learning Dashboard)
learningDashboardFilesDir=/var/bigbluebutton/learning-dashboard

View File

@ -197,6 +197,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
<property name="notifyRecordingIsOn" value="${notifyRecordingIsOn}"/>
<property name="defaultKeepEvents" value="${defaultKeepEvents}"/>
<property name="allowRevealOfBBBVersion" value="${allowRevealOfBBBVersion}"/>
<property name="allowOverrideClientSettingsOnCreateCall" value="${allowOverrideClientSettingsOnCreateCall}"/>
</bean>
<bean id="presentationService" class="org.bigbluebutton.web.services.PresentationService">

View File

@ -44,6 +44,7 @@ import org.bigbluebutton.web.services.turn.StunServer
import org.bigbluebutton.web.services.turn.RemoteIceCandidate
import org.json.JSONArray
import javax.servlet.ServletRequest
class ApiController {
@ -63,6 +64,7 @@ class ApiController {
ResponseBuilder responseBuilder = initResponseBuilder()
ValidationService validationService
def initResponseBuilder = {
String protocol = this.getClass().getResource("").getProtocol();
if (Objects.equals(protocol, "jar")) {
@ -150,11 +152,25 @@ class ApiController {
Meeting newMeeting = paramsProcessorUtil.processCreateParams(params)
String requestBody = request.inputStream == null ? null : request.inputStream.text
requestBody = StringUtils.isEmpty(requestBody) ? null : requestBody
def xmlModules = processRequestXmlModules(requestBody)
// Set Client Settings Override
if(xmlModules.containsKey("clientSettingsOverride")) {
if(paramsProcessorUtil.getAllowOverrideClientSettingsOnCreateCall()) {
newMeeting.setOverrideClientSettings(xmlModules.get("clientSettingsOverride").text())
} else {
log.warn("Module `clientSettingsOverride` provided but this options is disabled by `allowOverrideClientSettingsOnCreateCall=false` config.");
}
}
ApiErrors errors = new ApiErrors()
if (meetingService.createMeeting(newMeeting)) {
// See if the request came with pre-uploading of presentation.
uploadDocuments(newMeeting, false); //
uploadDocuments(xmlModules, newMeeting, false); //
respondWithConference(newMeeting, null, null)
} else {
// Translate the external meeting id into an internal meeting id.
@ -1106,8 +1122,12 @@ class ApiController {
Meeting meeting = ServiceUtils.findMeetingFromMeetingID(params.meetingID);
if (meeting != null){
if (uploadDocuments(meeting, true)) {
if (meeting != null) {
String requestBody = request.inputStream == null ? null : request.inputStream.text
requestBody = StringUtils.isEmpty(requestBody) ? null : requestBody
def xmlModules = processRequestXmlModules(requestBody)
if (uploadDocuments(xmlModules, meeting, true)) {
withFormat {
xml {
render(text: responseBuilder.buildInsertDocumentResponse("Presentation is being uploaded", RESP_CODE_SUCCESS)
@ -1345,7 +1365,7 @@ class ApiController {
}
}
def uploadDocuments(conf, isFromInsertAPI) {
def uploadDocuments(xmlModules, conf, isFromInsertAPI) {
if (conf.getDisabledFeatures().contains("presentation")) {
log.warn("Presentation feature is disabled.")
return false
@ -1365,8 +1385,6 @@ class ApiController {
}
Boolean isDefaultPresentationUsed = false;
String requestBody = request.inputStream == null ? null : request.inputStream.text;
requestBody = StringUtils.isEmpty(requestBody) ? null : requestBody;
Boolean isDefaultPresentationCurrent = false;
def listOfPresentation = []
def presentationListHasCurrent = false
@ -1374,40 +1392,43 @@ class ApiController {
// This part of the code is responsible for organize the presentations in a certain order
// It selects the one that has the current=true, and put it in the 0th place.
// Afterwards, the 0th presentation is going to be uploaded first, which spares processing time
if (requestBody == null) {
if (!xmlModules.containsKey("presentation")) {
if (isFromInsertAPI) {
log.warn("Insert Document API called without a payload - ignoring")
return;
}
listOfPresentation << [name: "default", current: true];
} else {
def xml = new XmlSlurper().parseText(requestBody);
Boolean hasCurrent = false;
xml.children().each { module ->
log.debug("module config found: [${module.@name}]");
if ("presentation".equals(module.@name.toString())) {
for (document in module.children()) {
if (!StringUtils.isEmpty(document.@current.toString()) && java.lang.Boolean.parseBoolean(
document.@current.toString()) && !hasCurrent) {
listOfPresentation.add(0, document)
hasCurrent = true;
} else {
listOfPresentation << document
}
}
Boolean uploadDefault = !preUploadedPresentationOverrideDefault && !isDefaultPresentationUsed && !isFromInsertAPI;
if (uploadDefault) {
isDefaultPresentationCurrent = !hasCurrent;
hasCurrent = true
isDefaultPresentationUsed = true
if (isDefaultPresentationCurrent) {
listOfPresentation.add(0, [name: "default", current: true])
} else {
listOfPresentation << [name: "default", current: false];
}
Boolean hasPresentationModule = false;
if (xmlModules.containsKey("presentation")) {
def modulePresentation = xmlModules.get("presentation")
hasPresentationModule = true
for (document in modulePresentation.children()) {
if (!StringUtils.isEmpty(document.@current.toString()) && java.lang.Boolean.parseBoolean(
document.@current.toString()) && !hasCurrent) {
listOfPresentation.add(0, document)
hasCurrent = true;
} else {
listOfPresentation << document
}
}
Boolean uploadDefault = !preUploadedPresentationOverrideDefault && !isDefaultPresentationUsed && !isFromInsertAPI;
if (uploadDefault) {
isDefaultPresentationCurrent = !hasCurrent;
hasCurrent = true
isDefaultPresentationUsed = true
if (isDefaultPresentationCurrent) {
listOfPresentation.add(0, [name: "default", current: true])
} else {
listOfPresentation << [name: "default", current: false];
}
}
}
if (!hasPresentationModule) {
hasCurrent = true
listOfPresentation.add(0, [name: "default", current: true])
}
presentationListHasCurrent = hasCurrent;
}
@ -1470,6 +1491,20 @@ class ApiController {
return true
}
def processRequestXmlModules(String requestBody) {
def xmlModules = [:]
if (requestBody != null && requestBody != "") {
def xml = new XmlSlurper().parseText(requestBody)
xml.children().each { module ->
log.debug("module found: [${module.@name}]")
xmlModules.put(module.@name.toString(), module);
}
}
return xmlModules
}
def processDocumentFromRawBytes(bytes, presOrigFilename, meetingId, current, isDownloadable, isRemovable,
isInitialPresentation) {
def uploadFailed = false