diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/ApiService.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/ApiService.scala index 1184eafc87..ecd382e69b 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/ApiService.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/ApiService.scala @@ -4,8 +4,8 @@ import org.apache.pekko.http.scaladsl.model._ import org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport import org.apache.pekko.http.scaladsl.server.Directives._ import org.bigbluebutton.common2.msgs._ -import org.bigbluebutton.core.api.{ApiResponseFailure, ApiResponseSuccess, UserInfosApiMsg} -import org.bigbluebutton.service.{HealthzService, MeetingInfoService, PubSubReceiveStatus, PubSubSendStatus, RecordingDBSendStatus, UserInfoService} +import org.bigbluebutton.core.api.{ ApiResponseFailure, ApiResponseSuccess, UserInfosApiMsg } +import org.bigbluebutton.service.{ HealthzService, MeetingInfoService, PubSubReceiveStatus, PubSubSendStatus, RecordingDBSendStatus, UserInfoService } import spray.json._ import scala.concurrent._ @@ -128,22 +128,19 @@ class ApiService(healthz: HealthzService, meetingInfoz: MeetingInfoService, user } } ~ path("userInfo") { - parameter( - "meetingId".as[String], - "userId".as[String], - ) { (meetingId, userId) => - get { - val entityFuture = userInfoService.getUserInfo(meetingId, userId).map { - case ApiResponseSuccess(msg, userInfos: UserInfosApiMsg) => - val responseMap = userInfoService.generateResponseMap(userInfos) - userInfoService.createHttpResponse(StatusCodes.OK, responseMap) + (headerValueByName("x-session-token") & headerValueByName("user-agent")) { (sessionToken, userAgent) => + get { + val entityFuture = userInfoService.getUserInfo(sessionToken).map { + case ApiResponseSuccess(msg, userInfos: UserInfosApiMsg) => + val responseMap = userInfoService.generateResponseMap(userInfos) + userInfoService.createHttpResponse(StatusCodes.OK, responseMap) - case ApiResponseFailure(msg, arg) => - userInfoService.createHttpResponse(StatusCodes.OK, Map("response" -> "unauthorized", "message" -> msg)) - } - - complete(entityFuture) + case ApiResponseFailure(msg, arg) => + userInfoService.createHttpResponse(StatusCodes.OK, Map("response" -> "unauthorized", "message" -> msg)) } + + complete(entityFuture) } + } } } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/BigBlueButtonActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/BigBlueButtonActor.scala index 5b709f4f06..c8fec85446 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/BigBlueButtonActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/BigBlueButtonActor.scala @@ -15,6 +15,7 @@ import java.util.concurrent.TimeUnit import org.bigbluebutton.common2.msgs._ import org.bigbluebutton.core.db.{ DatabaseConnection, MeetingDAO } import org.bigbluebutton.core.domain.MeetingEndReason +import org.bigbluebutton.core.models.Roles import org.bigbluebutton.core.running.RunningMeeting import org.bigbluebutton.core.util.ColorPicker import org.bigbluebutton.core2.RunningMeetings @@ -45,6 +46,8 @@ class BigBlueButtonActor( private val meetings = new RunningMeetings + private var sessionTokens = new collection.immutable.HashMap[String, (String, String)] //sessionToken -> (meetingId, userId) + override val supervisorStrategy = OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute) { case e: Exception => { val sw: StringWriter = new StringWriter() @@ -79,13 +82,39 @@ class BigBlueButtonActor( case _ => // do nothing } - def handleGetUserApiMsg(msg: GetUserApiMsg, actorRef: ActorRef): Unit = { + private def handleGetUserApiMsg(msg: GetUserApiMsg, actorRef: ActorRef): Unit = { log.debug("RECEIVED GetUserApiMsg msg {}", msg) - RunningMeetings.findWithId(meetings, msg.meetingId) match { - case Some(m) => - m.actorRef forward (msg) + sessionTokens.get(msg.sessionToken) match { + case Some(sessionTokenInfo) => + RunningMeetings.findWithId(meetings, sessionTokenInfo._1) match { + case Some(m) => + m.actorRef forward (msg) + case None => + //The meeting is ended, it will return some data just to confirm the session was valid + //The client can request data after the meeting is ended + val userInfos = Map( + "returncode" -> "SUCCESS", + "sessionToken" -> msg.sessionToken, + "meetingID" -> sessionTokenInfo._1, + "internalUserID" -> sessionTokenInfo._2, + "externMeetingID" -> "", + "externUserID" -> "", + "online" -> false, + "authToken" -> "", + "role" -> Roles.VIEWER_ROLE, + "guest" -> "false", + "guestStatus" -> "ALLOWED", + "moderator" -> false, + "presenter" -> false, + "hideViewersCursor" -> false, + "hideViewersAnnotation" -> false, + "hideUserList" -> false, + "webcamsOnlyForModerator" -> false + ) + actorRef ! ApiResponseSuccess("Meeting is ended!", UserInfosApiMsg(userInfos)) + } case None => actorRef ! ApiResponseFailure("Meeting not found!") } @@ -125,6 +154,10 @@ class BigBlueButtonActor( m <- RunningMeetings.findWithId(meetings, msg.header.meetingId) } yield { log.debug("FORWARDING Register user message") + + //Store sessionTokens and associate them with their respective meetingId + userId owners + sessionTokens += (msg.body.sessionToken -> (msg.body.meetingId, msg.body.intUserId)) + m.actorRef forward (msg) } } @@ -209,11 +242,15 @@ class BigBlueButtonActor( context.stop(m.actorRef) } - // MeetingDAO.delete(msg.meetingId) - // MeetingDAO.setMeetingEnded(msg.meetingId) - // Removing the meeting is enough, all other tables has "ON DELETE CASCADE" - // UserDAO.softDeleteAllFromMeeting(msg.meetingId) - // MeetingRecordingDAO.updateStopped(msg.meetingId, "") + //Delay removal of session tokens and Graphql data once users might request some info after the meeting is ended + context.system.scheduler.scheduleOnce(Duration.create(60, TimeUnit.MINUTES)) { + log.debug("Removing Graphql data and session tokens. meetingID={}", msg.meetingId) + + sessionTokens = sessionTokens.filter(sessionTokenInfo => sessionTokenInfo._2._1 != msg.meetingId) + + //In Db, Removing the meeting is enough, all other tables has "ON DELETE CASCADE" + MeetingDAO.delete(msg.meetingId) + } //Remove ColorPicker idx of the meeting ColorPicker.reset(m.props.meetingProp.intId) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/api/InMessages.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/api/InMessages.scala index 73387eaa5b..7a5709c560 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/api/InMessages.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/api/InMessages.scala @@ -141,7 +141,7 @@ case class UserEstablishedGraphqlConnectionInternalMsg(userId: String, clientTyp /** * API endpoint /userInfo to provide User Session Variables messages */ -case class GetUserApiMsg(meetingId: String, userIntId: String) +case class GetUserApiMsg(sessionToken: String) case class UserInfosApiMsg(infos: Map[String, Any]) trait ApiResponse diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/GetUserApiMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/GetUserApiMsgHdlr.scala index 6412ceab9f..ae464bacb7 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/GetUserApiMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/GetUserApiMsgHdlr.scala @@ -2,7 +2,7 @@ package org.bigbluebutton.core.apps.users import org.apache.pekko.actor.ActorRef import org.bigbluebutton.core.api.{ ApiResponseFailure, ApiResponseSuccess, GetUserApiMsg, UserInfosApiMsg } -import org.bigbluebutton.core.models.{ RegisteredUsers, Roles, Users2x } +import org.bigbluebutton.core.models.{ RegisteredUser, RegisteredUsers, Roles, Users2x } import org.bigbluebutton.core.running.{ HandlerHelpers, LiveMeeting, OutMsgRouter } import org.bigbluebutton.core2.MeetingStatus2x @@ -13,52 +13,52 @@ trait GetUserApiMsgHdlr extends HandlerHelpers { val outGW: OutMsgRouter def handleGetUsersMeetingReqMsg(msg: GetUserApiMsg, actorRef: ActorRef): Unit = { - - val userOption = RegisteredUsers.findWithUserId(msg.userIntId, liveMeeting.registeredUsers) - - userOption match { + RegisteredUsers.findWithSessionToken(msg.sessionToken, liveMeeting.registeredUsers) match { case Some(regUser) => log.debug("replying GetUserApiMsg with success") - - val isModerator = (regUser.role == Roles.MODERATOR_ROLE) - val isLocked = Users2x.findWithIntId(liveMeeting.users2x, regUser.id).exists(u => u.locked) - val userStateExists = Users2x.findWithIntId(liveMeeting.users2x, regUser.id).nonEmpty - - val userIsOnline = regUser.joined && !regUser.loggedOut && userStateExists - - val permissions = MeetingStatus2x.getPermissions(liveMeeting.status) - - var userInfos: Map[String, Any] = Map() - userInfos += ("returncode" -> "SUCCESS") - userInfos += ("meetingID" -> liveMeeting.props.meetingProp.intId) - userInfos += ("externMeetingID" -> liveMeeting.props.meetingProp.extId) - userInfos += ("externUserID" -> regUser.externId) - userInfos += ("internalUserID" -> regUser.id) - userInfos += ("online" -> userIsOnline) - userInfos += ("authToken" -> regUser.authToken) - userInfos += ("sessionToken" -> regUser.sessionToken) - userInfos += ("role" -> regUser.role) - userInfos += ("guest" -> regUser.guest) - userInfos += ("guestStatus" -> regUser.guestStatus) - userInfos += ("moderator" -> isModerator) - userInfos += ("presenter" -> Users2x.userIsInPresenterGroup(liveMeeting.users2x, regUser.id)) - if (isModerator || !isLocked) { - userInfos += ("hideViewersCursor" -> false) - userInfos += ("hideViewersAnnotation" -> false) - userInfos += ("hideUserList" -> false) - userInfos += ("webcamsOnlyForModerator" -> false) - } else { - userInfos += ("hideViewersCursor" -> permissions.hideViewersCursor) - userInfos += ("hideViewersAnnotation" -> permissions.hideViewersAnnotation) - userInfos += ("hideUserList" -> permissions.hideUserList) - userInfos += ("webcamsOnlyForModerator" -> MeetingStatus2x.webcamsOnlyForModeratorEnabled(liveMeeting.status)) - } - - actorRef ! ApiResponseSuccess("User found!", UserInfosApiMsg(userInfos)) - + actorRef ! ApiResponseSuccess("User found!", UserInfosApiMsg(getUserInfoResponse(regUser))) case None => log.debug("User not found, sending failure message") actorRef ! ApiResponseFailure("User not found", Map()) } } + + private def getUserInfoResponse(regUser: RegisteredUser): Map[String, Any] = { + val isModerator = (regUser.role == Roles.MODERATOR_ROLE) + val isLocked = Users2x.findWithIntId(liveMeeting.users2x, regUser.id).exists(u => u.locked) + val userStateExists = Users2x.findWithIntId(liveMeeting.users2x, regUser.id).nonEmpty + + val userIsOnline = regUser.joined && !regUser.loggedOut && !regUser.ejected && userStateExists + + val permissions = MeetingStatus2x.getPermissions(liveMeeting.status) + + var userInfos: Map[String, Any] = Map() + userInfos += ("returncode" -> "SUCCESS") + userInfos += ("meetingID" -> liveMeeting.props.meetingProp.intId) + userInfos += ("externMeetingID" -> liveMeeting.props.meetingProp.extId) + userInfos += ("externUserID" -> regUser.externId) + userInfos += ("internalUserID" -> regUser.id) + userInfos += ("online" -> userIsOnline) + userInfos += ("authToken" -> regUser.authToken) + userInfos += ("sessionToken" -> regUser.sessionToken) + userInfos += ("role" -> regUser.role) + userInfos += ("guest" -> regUser.guest) + userInfos += ("guestStatus" -> regUser.guestStatus) + userInfos += ("moderator" -> isModerator) + userInfos += ("presenter" -> Users2x.userIsInPresenterGroup(liveMeeting.users2x, regUser.id)) + if (isModerator || !isLocked) { + userInfos += ("hideViewersCursor" -> false) + userInfos += ("hideViewersAnnotation" -> false) + userInfos += ("hideUserList" -> false) + userInfos += ("webcamsOnlyForModerator" -> false) + } else { + userInfos += ("hideViewersCursor" -> permissions.hideViewersCursor) + userInfos += ("hideViewersAnnotation" -> permissions.hideViewersAnnotation) + userInfos += ("hideUserList" -> permissions.hideUserList) + userInfos += ("webcamsOnlyForModerator" -> MeetingStatus2x.webcamsOnlyForModeratorEnabled(liveMeeting.status)) + } + + userInfos + } + } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/RegisterUserReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/RegisterUserReqMsgHdlr.scala index b3a96abd82..ee2cf9decc 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/RegisterUserReqMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/RegisterUserReqMsgHdlr.scala @@ -30,6 +30,7 @@ trait RegisterUserReqMsgHdlr { if (liveMeeting.props.usersProp.maxUserConcurrentAccesses > 0) { val userConcurrentAccesses = RegisteredUsers.findAllWithExternUserId(regUser.externId, liveMeeting.registeredUsers) .filter(u => !u.loggedOut) + .filter(u => !u.ejected) .sortWith((u1, u2) => u1.registeredOn > u2.registeredOn) //Remove older first val userAvailableSlots = liveMeeting.props.usersProp.maxUserConcurrentAccesses - userConcurrentAccesses.length diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UserJoinMeetingReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UserJoinMeetingReqMsgHdlr.scala index a684689b59..11d15092e0 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UserJoinMeetingReqMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UserJoinMeetingReqMsgHdlr.scala @@ -34,6 +34,7 @@ trait UserJoinMeetingReqMsgHdlr extends HandlerHelpers { val validationResult = for { _ <- checkIfUserGuestStatusIsAllowed(user) _ <- checkIfUserIsBanned(user) + _ <- checkIfUserEjected(user) _ <- checkIfUserLoggedOut(user) _ <- validateMaxParticipants(user) } yield user @@ -104,6 +105,14 @@ trait UserJoinMeetingReqMsgHdlr extends HandlerHelpers { } } + private def checkIfUserEjected(user: RegisteredUser): Either[(String, String), Unit] = { + if (user.ejected) { + Left(("User had ejected", EjectReasonCode.EJECT_USER)) + } else { + Right(()) + } + } + private def checkIfUserLoggedOut(user: RegisteredUser): Either[(String, String), Unit] = { if (user.loggedOut) { Left(("User had logged out", EjectReasonCode.USER_LOGGED_OUT)) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ValidateAuthTokenReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ValidateAuthTokenReqMsgHdlr.scala index e2d063c19f..8fa0fecf1d 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ValidateAuthTokenReqMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ValidateAuthTokenReqMsgHdlr.scala @@ -27,6 +27,7 @@ trait ValidateAuthTokenReqMsgHdlr extends HandlerHelpers { val validationResult = for { _ <- checkIfUserGuestStatusIsAllowed(user) _ <- checkIfUserIsBanned(user) + _ <- checkIfUserEjected(user) _ <- checkIfUserLoggedOut(user) _ <- validateMaxParticipants(user) } yield user @@ -66,6 +67,14 @@ trait ValidateAuthTokenReqMsgHdlr extends HandlerHelpers { } } + private def checkIfUserEjected(user: RegisteredUser): Either[(String, String), Unit] = { + if (user.ejected) { + Left(("User had ejected", EjectReasonCode.EJECT_USER)) + } else { + Right(()) + } + } + private def checkIfUserLoggedOut(user: RegisteredUser): Either[(String, String), Unit] = { if (user.loggedOut) { Left(("User had logged out", EjectReasonCode.USER_LOGGED_OUT)) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/RegisteredUsers.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/RegisteredUsers.scala index bc491f4a9c..a0244771be 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/RegisteredUsers.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/RegisteredUsers.scala @@ -24,11 +24,12 @@ object RegisteredUsers { guestStatus, excludeFromDashboard, System.currentTimeMillis(), - 0, - false, - 0, - false, - false, + lastAuthTokenValidatedOn = 0, + graphqlConnected = false, + graphqlDisconnectedOn = 0, + joined = false, + ejected = false, + banned = false, enforceLayout, customParameters, loggedOut, @@ -39,6 +40,10 @@ object RegisteredUsers { users.toVector.find(u => u.authToken == token) } + def findWithSessionToken(sessionToken: String, users: RegisteredUsers): Option[RegisteredUser] = { + users.toVector.find(u => u.sessionToken == sessionToken) + } + def findAll(users: RegisteredUsers): Vector[RegisteredUser] = { users.toVector } @@ -128,9 +133,10 @@ object RegisteredUsers { UserDAO.update(u) u } else { - users.delete(ejectedUser.id) - UserDAO.softDelete(ejectedUser.meetingId, ejectedUser.id) - ejectedUser + val u = ejectedUser.modify(_.ejected).setTo(true) + users.save(u) + + updateUserJoin(users, u, joined = false) } } @@ -243,6 +249,7 @@ case class RegisteredUser( graphqlConnected: Boolean, graphqlDisconnectedOn: Long, joined: Boolean, + ejected: Boolean, banned: Boolean, enforceLayout: String, customParameters: Map[String,String], diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala index 49f2b81e49..5c52656c13 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala @@ -635,7 +635,7 @@ class MeetingActor( case m: SetCurrentPagePubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus) updateUserLastActivity(m.header.userId) - case m: SetPageInfiniteWhiteboardPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus) + case m: SetPageInfiniteWhiteboardPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus) case m: RemovePresentationPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus) case m: SetPresentationDownloadablePubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus) case m: PresentationConversionUpdateSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus) @@ -961,6 +961,7 @@ class MeetingActor( regUser <- RegisteredUsers.findAll(liveMeeting.registeredUsers) } yield { if (!regUser.loggedOut + && !regUser.ejected && regUser.guestStatus == GuestStatus.WAIT && !regUser.graphqlConnected && regUser.graphqlDisconnectedOn != 0) { diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/service/UserInfoService.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/service/UserInfoService.scala index f0fad82c88..2dac8c43fa 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/service/UserInfoService.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/service/UserInfoService.scala @@ -1,16 +1,16 @@ package org.bigbluebutton.service -import org.apache.pekko.actor.{ ActorRef, ActorSystem } +import org.apache.pekko.actor.{ActorRef, ActorSystem} import org.apache.pekko.http.scaladsl.model.headers.RawHeader -import org.apache.pekko.http.scaladsl.model.{ ContentTypes, HttpEntity, HttpResponse, StatusCode } +import org.apache.pekko.http.scaladsl.model.{ContentTypes, HttpEntity, HttpResponse, StatusCode} import org.apache.pekko.pattern.ask import org.apache.pekko.pattern.AskTimeoutException import org.apache.pekko.util.Timeout import org.bigbluebutton.common2.util.JsonUtil -import org.bigbluebutton.core.api.{ ApiResponse, ApiResponseFailure, GetUserApiMsg, UserInfosApiMsg } +import org.bigbluebutton.core.api.{ApiResponse, ApiResponseFailure, GetUserApiMsg, UserInfosApiMsg} import scala.concurrent.duration.DurationInt -import scala.concurrent.{ ExecutionContextExecutor, Future } +import scala.concurrent.{ExecutionContextExecutor, Future} object UserInfoService { @@ -21,8 +21,8 @@ class UserInfoService(system: ActorSystem, bbbActor: ActorRef) { implicit def executionContext: ExecutionContextExecutor = system.dispatcher implicit val timeout: Timeout = 2 seconds - def getUserInfo(meetingId: String, userIntId: String): Future[ApiResponse] = { - val future = bbbActor.ask(GetUserApiMsg(meetingId, userIntId)).mapTo[ApiResponse] + def getUserInfo(sessionToken: String): Future[ApiResponse] = { + val future = bbbActor.ask(GetUserApiMsg(sessionToken)).mapTo[ApiResponse] future.recover { case e: AskTimeoutException => ApiResponseFailure("Request Timeout error") diff --git a/bbb-graphql-middleware/internal/akka_apps/client.go b/bbb-graphql-middleware/internal/akka_apps/client.go index 7c1c023963..f5cd3a4aa5 100644 --- a/bbb-graphql-middleware/internal/akka_apps/client.go +++ b/bbb-graphql-middleware/internal/akka_apps/client.go @@ -13,10 +13,10 @@ import ( // sessionVarsHookUrl is the authentication hook URL obtained from an environment variable. var sessionVarsHookUrl = os.Getenv("BBB_GRAPHQL_MIDDLEWARE_SESSION_VARS_HOOK_URL") -func AkkaAppsGetSessionVariablesFrom(browserConnectionId string, meetingId string, userId string) (map[string]string, error) { - logger := log.WithField("_routine", "BBBWebClient").WithField("browserConnectionId", browserConnectionId) - logger.Debug("Starting BBBWebClient") - defer logger.Debug("Finished BBBWebClient") +func AkkaAppsGetSessionVariablesFrom(browserConnectionId string, sessionToken string) (map[string]string, error) { + logger := log.WithField("_routine", "AkkaAppsClient").WithField("browserConnectionId", browserConnectionId) + logger.Debug("Starting AkkaAppsClient") + defer logger.Debug("Finished AkkaAppsClient") // Create a new HTTP client with a cookie jar. client := &http.Client{} @@ -26,15 +26,17 @@ func AkkaAppsGetSessionVariablesFrom(browserConnectionId string, meetingId strin return nil, fmt.Errorf("BBB_GRAPHQL_MIDDLEWARE_SESSION_VARS_HOOK_URL not set") } - log.Trace("Get user session vars from: " + sessionVarsHookUrl + "?meetingId=" + meetingId + "&userId=" + userId) + log.Trace("Get user session vars from: " + sessionVarsHookUrl + "?sessionToken=" + sessionToken) // Create a new HTTP request to the authentication hook URL. - req, err := http.NewRequest("GET", sessionVarsHookUrl+"?meetingId="+meetingId+"&userId="+userId, nil) + req, err := http.NewRequest("GET", sessionVarsHookUrl, nil) if err != nil { return nil, err } // Execute the HTTP request to obtain user session variables (like X-Hasura-Role) + req.Header.Set("x-session-token", sessionToken) + req.Header.Set("User-Agent", "bbb-graphql-middleware") resp, err := client.Do(req) if err != nil { return nil, err diff --git a/bbb-graphql-middleware/internal/bbb_web/client.go b/bbb-graphql-middleware/internal/bbb_web/client.go index 02c4a79286..fbececf2ad 100644 --- a/bbb-graphql-middleware/internal/bbb_web/client.go +++ b/bbb-graphql-middleware/internal/bbb_web/client.go @@ -1,12 +1,14 @@ package bbb_web import ( + "encoding/json" "fmt" log "github.com/sirupsen/logrus" "io/ioutil" "net/http" "net/http/cookiejar" "os" + "strings" ) // authHookUrl is the authentication hook URL obtained from an environment variable. @@ -41,7 +43,8 @@ func BBBWebCheckAuthorization(browserConnectionId string, sessionToken string, c } // Execute the HTTP request to obtain user session variables (like X-Hasura-Role) - req.Header.Set("x-original-uri", authHookUrl+"?sessionToken="+sessionToken) + //req.Header.Set("x-original-uri", authHookUrl+"?sessionToken="+sessionToken) + req.Header.Set("x-session-token", sessionToken) //req.Header.Set("User-Agent", "hasura-graphql-engine") resp, err := client.Do(req) if err != nil { @@ -54,27 +57,44 @@ func BBBWebCheckAuthorization(browserConnectionId string, sessionToken string, c return "", "", err } - var respBodyAsString = string(respBody) + log.Trace(string(respBody)) - if respBodyAsString != "authorized" { - return "", "", fmt.Errorf("auth token not authorized") + var respBodyAsMap map[string]string + if err := json.Unmarshal(respBody, &respBodyAsMap); err != nil { + return "", "", err + } + + // Check the response status. + response, ok := respBodyAsMap["response"] + if !ok { + return "", "", fmt.Errorf("response key not found in the parsed object") + } + if response != "authorized" { + logger.Error(response) + return "", "", fmt.Errorf("user not authorized") + } + + // Normalize the response header keys. + normalizedResponse := make(map[string]string) + for key, value := range respBodyAsMap { + if strings.HasPrefix(strings.ToLower(key), "x-") { + normalizedResponse[strings.ToLower(key)] = value + } } var userId string var meetingId string //Get userId and meetingId from response Header - for key, values := range resp.Header { - for _, value := range values { - log.Debug("%s: %s\n", key, value) + for key, value := range normalizedResponse { + log.Debug("%s: %s\n", key, value) - if key == "User-Id" { - userId = value - } + if key == "x-userid" { + userId = value + } - if key == "Meeting-Id" { - meetingId = value - } + if key == "x-meetingid" { + meetingId = value } } diff --git a/bbb-graphql-middleware/internal/websrv/connhandler.go b/bbb-graphql-middleware/internal/websrv/connhandler.go index 23dbe95404..133958ebc8 100644 --- a/bbb-graphql-middleware/internal/websrv/connhandler.go +++ b/bbb-graphql-middleware/internal/websrv/connhandler.go @@ -240,13 +240,12 @@ func invalidateHasuraConnectionForSessionToken(bc *common.BrowserConnection, ses func refreshUserSessionVariables(browserConnection *common.BrowserConnection) error { BrowserConnectionsMutex.RLock() - browserUserId := browserConnection.UserId - browserMeetingId := browserConnection.MeetingId + sessionToken := browserConnection.SessionToken browserConnectionId := browserConnection.Id BrowserConnectionsMutex.RUnlock() // Check authorization - sessionVariables, err := akka_apps.AkkaAppsGetSessionVariablesFrom(browserConnectionId, browserMeetingId, browserUserId) + sessionVariables, err := akka_apps.AkkaAppsGetSessionVariablesFrom(browserConnectionId, sessionToken) if err != nil { log.Error(err) return fmt.Errorf("error on checking sessionToken authorization") diff --git a/bbb-graphql-server/metadata/actions.graphql b/bbb-graphql-server/metadata/actions.graphql index bbf7f5723a..bf359891d8 100644 --- a/bbb-graphql-server/metadata/actions.graphql +++ b/bbb-graphql-server/metadata/actions.graphql @@ -272,8 +272,8 @@ type Mutation { type Mutation { pluginLearningAnalyticsDashboardSendGenericData( - genericDataForLearningAnalyticsDashboard: json!, - pluginName: String!, + genericDataForLearningAnalyticsDashboard: json! + pluginName: String! ): Boolean } diff --git a/bbb-graphql-server/metadata/query_collections.yaml b/bbb-graphql-server/metadata/query_collections.yaml index cfbe507e36..4a3102c78f 100644 --- a/bbb-graphql-server/metadata/query_collections.yaml +++ b/bbb-graphql-server/metadata/query_collections.yaml @@ -1,23 +1,6 @@ - name: allowed-queries definition: queries: - - name: UserCurrent - query: | - query UserCurrent { - user_current { - userId - name - guestStatus - guestStatusDetails { - guestLobbyMessage - positionInWaitingQueue - } - meeting { - name - logoutUrl - } - } - } - name: clientStartupSettings query: | query clientStartupSettings { diff --git a/bbb-graphql-server/metadata/rest_endpoints.yaml b/bbb-graphql-server/metadata/rest_endpoints.yaml index 8cc0bf46cc..c34fa55f02 100644 --- a/bbb-graphql-server/metadata/rest_endpoints.yaml +++ b/bbb-graphql-server/metadata/rest_endpoints.yaml @@ -16,12 +16,3 @@ - GET name: clientStartupSettings url: clientStartupSettings -- comment: "" - definition: - query: - collection_name: allowed-queries - query_name: UserCurrent - methods: - - GET - name: UserCurrent - url: usercurrent diff --git a/bigbluebutton-html5/imports/ui/components/connection-manager/startup-data-fetch/component.tsx b/bigbluebutton-html5/imports/ui/components/connection-manager/startup-data-fetch/component.tsx index d200fdab4c..25fd17fa29 100644 --- a/bigbluebutton-html5/imports/ui/components/connection-manager/startup-data-fetch/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/connection-manager/startup-data-fetch/component.tsx @@ -76,8 +76,14 @@ const StartupDataFetch: React.FC = ({ }, }).then((resp) => resp.json()) .then((data) => { - const url = `${data.response.graphqlApiUrl}/clientStartupSettings/?sessionToken=${sessionToken}`; - fetch(url, { method: 'get', credentials: 'include' }) + const url = `${data.response.graphqlApiUrl}/clientStartupSettings`; + fetch(url, { + method: 'get', + credentials: 'include', + headers: { + 'x-session-token': sessionToken, + }, + }) .then((resp) => resp.json()) .then((data: Response) => { const settings = data.meeting_clientSettings[0]; diff --git a/bigbluebutton-html5/imports/ui/components/settings-loader/component.tsx b/bigbluebutton-html5/imports/ui/components/settings-loader/component.tsx index e441ee8d77..394a7ced7f 100644 --- a/bigbluebutton-html5/imports/ui/components/settings-loader/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/settings-loader/component.tsx @@ -40,9 +40,14 @@ const SettingsLoader: React.FC = () => { .then((data) => { const urlParams = new URLSearchParams(window.location.search); const sessionToken = urlParams.get('sessionToken'); - const clientStartupSettings = `/clientSettings/?sessionToken=${sessionToken}`; - const url = new URL(`${data.response.graphqlApiUrl}${clientStartupSettings}`); - fetch(url, { method: 'get', credentials: 'include' }) + const url = new URL(`${data.response.graphqlApiUrl}/clientSettings`); + fetch(url, { + method: 'get', + credentials: 'include', + headers: { + 'x-session-token': sessionToken, + }, + }) .then((resp) => resp.json()) .then((data: Response) => { const settings = data?.meeting_clientSettings[0].clientSettingsJson; diff --git a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ConnectionController.groovy b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ConnectionController.groovy index a735b30934..78ca7df618 100755 --- a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ConnectionController.groovy +++ b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ConnectionController.groovy @@ -62,72 +62,51 @@ class ConnectionController { def checkGraphqlAuthorization = { try { - if(!request.getHeader("User-Agent").startsWith('hasura-graphql-engine')) { - throw new Exception("Invalid User Agent") - } - String sessionToken = request.getHeader("x-session-token") UserSession userSession = meetingService.getUserSessionWithSessionToken(sessionToken) - Boolean allowRequestsWithoutSession = meetingService.getAllowRequestsWithoutSession(sessionToken) - Boolean isSessionTokenInvalid = !session[sessionToken] && !allowRequestsWithoutSession + Boolean isSessionTokenValid = session[sessionToken] != null response.addHeader("Cache-Control", "no-cache") - if (userSession != null && !isSessionTokenInvalid) { + if (userSession != null && isSessionTokenValid) { Meeting m = meetingService.getMeeting(userSession.meetingID) User u if(m) { u = m.getUserById(userSession.internalUserId) } - Boolean cursorLocked = false - Boolean annotationsLocked = false - Boolean userListLocked = false - Boolean webcamOnlyForMod = false - if(u && u.isLocked() && !u.isModerator()) { - cursorLocked = m.lockSettingsParams.hideViewersCursor - annotationsLocked = m.lockSettingsParams.hideViewersAnnotation - userListLocked = m.lockSettingsParams.hideUserList - webcamOnlyForMod = m.getWebcamsOnlyForModerator() - } - + response.addHeader("Meeting-Id", userSession.meetingID) response.setStatus(200) withFormat { json { def builder = new JsonBuilder() builder { "response" "authorized" - "X-Hasura-Role" m && u && !u.hasLeft() ? "bbb_client" : "not_joined_bbb_client" - "X-Hasura-ModeratorInMeeting" u && u.isModerator() ? userSession.meetingID : "" - "X-Hasura-PresenterInMeeting" u && u.isPresenter() ? userSession.meetingID : "" - "X-Hasura-UserId" userSession.internalUserId - "X-Hasura-MeetingId" userSession.meetingID - "X-Hasura-CursorNotLockedInMeeting" cursorLocked ? "" : userSession.meetingID - "X-Hasura-CursorLockedUserId" cursorLocked ? userSession.internalUserId : "" - "X-Hasura-AnnotationsNotLockedInMeeting" annotationsLocked ? "" : userSession.meetingID - "X-Hasura-AnnotationsLockedUserId" annotationsLocked ? userSession.internalUserId : "" - "X-Hasura-UserListNotLockedInMeeting" userListLocked ? "" : userSession.meetingID - "X-Hasura-WebcamsNotLockedInMeeting" webcamOnlyForMod ? "" : userSession.meetingID - "X-Hasura-WebcamsLockedUserId" webcamOnlyForMod ? userSession.internalUserId : "" + "X-Currently-Online" m && u && !u.hasLeft() ? "true" : "false" + "X-Moderator" u && u.isModerator() ? "true" : "false" + "X-Presenter" u && u.isPresenter() ? "true" : "false" + "X-UserId" userSession.internalUserId + "X-MeetingId" userSession.meetingID } render(contentType: "application/json", text: builder.toPrettyString()) } } - } else { + } else if(isSessionTokenValid) { UserSessionBasicData removedUserSession = meetingService.getRemovedUserSessionWithSessionToken(sessionToken) if(removedUserSession) { + response.addHeader("Meeting-Id", removedUserSession.meetingId) response.setStatus(200) withFormat { json { def builder = new JsonBuilder() builder { "response" "authorized" - "X-Hasura-Role" "not_joined_bbb_client" - "X-Hasura-ModeratorInMeeting" removedUserSession.isModerator() ? removedUserSession.meetingId : "" - "X-Hasura-PresenterInMeeting" "" - "X-Hasura-UserId" removedUserSession.userId - "X-Hasura-MeetingId" removedUserSession.meetingId + "X-Currently-Online" "false" + "X-Moderator" removedUserSession.isModerator() ? "true" : "false" + "X-Presenter" "false" + "X-UserId" removedUserSession.userId + "X-MeetingId" removedUserSession.meetingId } render(contentType: "application/json", text: builder.toPrettyString()) } @@ -135,6 +114,8 @@ class ConnectionController { } else { throw new Exception("Invalid User Session") } + } else { + throw new Exception("Invalid sessionToken") } } catch (Exception e) { log.debug("Error while authenticating graphql connection: " + e.getMessage()) diff --git a/build/packages-template/bbb-graphql-middleware/bbb-graphql-middleware-config.env b/build/packages-template/bbb-graphql-middleware/bbb-graphql-middleware-config.env index 0821285ee8..6db0c35e21 100644 --- a/build/packages-template/bbb-graphql-middleware/bbb-graphql-middleware-config.env +++ b/build/packages-template/bbb-graphql-middleware/bbb-graphql-middleware-config.env @@ -4,7 +4,7 @@ BBB_GRAPHQL_MIDDLEWARE_REDIS_ADDRESS=127.0.0.1:6379 BBB_GRAPHQL_MIDDLEWARE_REDIS_PASSWORD= BBB_GRAPHQL_MIDDLEWARE_HASURA_WS=ws://127.0.0.1:8085/v1/graphql BBB_GRAPHQL_MIDDLEWARE_MAX_CONN_PER_SECOND=10 -BBB_GRAPHQL_MIDDLEWARE_AUTH_HOOK_URL=http://127.0.0.1:8090/bigbluebutton/connection/checkAuthorization +BBB_GRAPHQL_MIDDLEWARE_AUTH_HOOK_URL=http://127.0.0.1:8090/bigbluebutton/connection/checkGraphqlAuthorization BBB_GRAPHQL_MIDDLEWARE_SESSION_VARS_HOOK_URL=http://127.0.0.1:8901/userInfo BBB_GRAPHQL_MIDDLEWARE_GRAPHQL_ACTIONS_URL=http://127.0.0.1:8093 diff --git a/build/packages-template/bbb-graphql-middleware/graphql.nginx b/build/packages-template/bbb-graphql-middleware/graphql.nginx index 66f044edc3..999d76fc02 100644 --- a/build/packages-template/bbb-graphql-middleware/graphql.nginx +++ b/build/packages-template/bbb-graphql-middleware/graphql.nginx @@ -25,20 +25,9 @@ location /api/rest { #Set cache system for client settings location ~ ^/api/rest/(clientStartupSettings|clientSettings)/ { - #Store URL sessionToken once it will be injected as a header X-Session-Token - set $session_token ""; - if ($args ~* "(?:^|&)sessionToken=([^&]+)") { - set $session_token $1; - } - auth_request /bigbluebutton/connection/checkAuthorization; + auth_request /bigbluebutton/connection/checkGraphqlAuthorization; auth_request_set $meeting_id $sent_http_meeting_id; - #Remove sessionToken from URL once Hasura doesn't expect to receive it - if ($args ~* "^(.*&)?sessionToken=[^&]*(.*)$") { - set $args $1$2; - } - - proxy_set_header x-session-token $session_token; proxy_cache client_settings_cache; proxy_cache_key "$uri|$meeting_id"; proxy_cache_use_stale updating; diff --git a/build/packages-template/bbb-graphql-server/hasura-config.env b/build/packages-template/bbb-graphql-server/hasura-config.env index 9fc7635598..b6d04d301f 100644 --- a/build/packages-template/bbb-graphql-server/hasura-config.env +++ b/build/packages-template/bbb-graphql-server/hasura-config.env @@ -11,5 +11,5 @@ HASURA_GRAPHQL_SERVER_PORT=8085 HASURA_GRAPHQL_ADMIN_SECRET=bigbluebutton HASURA_GRAPHQL_ENABLE_TELEMETRY=false HASURA_GRAPHQL_WEBSOCKET_KEEPALIVE=10 -HASURA_GRAPHQL_AUTH_HOOK=http://127.0.0.1:8090/bigbluebutton/connection/checkGraphqlAuthorization +HASURA_GRAPHQL_AUTH_HOOK=http://127.0.0.1:8901/userInfo HASURA_BBB_GRAPHQL_ACTIONS_ADAPTER_URL=http://127.0.0.1:8093