Merge pull request #20686 from gustavotrott/gql-server-auth-session

refactor (graphql): Big refactor on Graphql authentication
This commit is contained in:
Gustavo Trott 2024-07-10 20:17:23 -03:00 committed by GitHub
commit 7a03136d02
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 225 additions and 188 deletions

View File

@ -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.marshallers.sprayjson.SprayJsonSupport
import org.apache.pekko.http.scaladsl.server.Directives._ import org.apache.pekko.http.scaladsl.server.Directives._
import org.bigbluebutton.common2.msgs._ import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.api.{ApiResponseFailure, ApiResponseSuccess, UserInfosApiMsg} import org.bigbluebutton.core.api.{ ApiResponseFailure, ApiResponseSuccess, UserInfosApiMsg }
import org.bigbluebutton.service.{HealthzService, MeetingInfoService, PubSubReceiveStatus, PubSubSendStatus, RecordingDBSendStatus, UserInfoService} import org.bigbluebutton.service.{ HealthzService, MeetingInfoService, PubSubReceiveStatus, PubSubSendStatus, RecordingDBSendStatus, UserInfoService }
import spray.json._ import spray.json._
import scala.concurrent._ import scala.concurrent._
@ -128,22 +128,19 @@ class ApiService(healthz: HealthzService, meetingInfoz: MeetingInfoService, user
} }
} ~ } ~
path("userInfo") { path("userInfo") {
parameter( (headerValueByName("x-session-token") & headerValueByName("user-agent")) { (sessionToken, userAgent) =>
"meetingId".as[String], get {
"userId".as[String], val entityFuture = userInfoService.getUserInfo(sessionToken).map {
) { (meetingId, userId) => case ApiResponseSuccess(msg, userInfos: UserInfosApiMsg) =>
get { val responseMap = userInfoService.generateResponseMap(userInfos)
val entityFuture = userInfoService.getUserInfo(meetingId, userId).map { userInfoService.createHttpResponse(StatusCodes.OK, responseMap)
case ApiResponseSuccess(msg, userInfos: UserInfosApiMsg) =>
val responseMap = userInfoService.generateResponseMap(userInfos)
userInfoService.createHttpResponse(StatusCodes.OK, responseMap)
case ApiResponseFailure(msg, arg) => case ApiResponseFailure(msg, arg) =>
userInfoService.createHttpResponse(StatusCodes.OK, Map("response" -> "unauthorized", "message" -> msg)) userInfoService.createHttpResponse(StatusCodes.OK, Map("response" -> "unauthorized", "message" -> msg))
}
complete(entityFuture)
} }
complete(entityFuture)
} }
}
} }
} }

View File

@ -15,6 +15,7 @@ import java.util.concurrent.TimeUnit
import org.bigbluebutton.common2.msgs._ import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.db.{ DatabaseConnection, MeetingDAO } import org.bigbluebutton.core.db.{ DatabaseConnection, MeetingDAO }
import org.bigbluebutton.core.domain.MeetingEndReason import org.bigbluebutton.core.domain.MeetingEndReason
import org.bigbluebutton.core.models.Roles
import org.bigbluebutton.core.running.RunningMeeting import org.bigbluebutton.core.running.RunningMeeting
import org.bigbluebutton.core.util.ColorPicker import org.bigbluebutton.core.util.ColorPicker
import org.bigbluebutton.core2.RunningMeetings import org.bigbluebutton.core2.RunningMeetings
@ -45,6 +46,8 @@ class BigBlueButtonActor(
private val meetings = new RunningMeetings 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) { override val supervisorStrategy = OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute) {
case e: Exception => { case e: Exception => {
val sw: StringWriter = new StringWriter() val sw: StringWriter = new StringWriter()
@ -79,13 +82,39 @@ class BigBlueButtonActor(
case _ => // do nothing case _ => // do nothing
} }
def handleGetUserApiMsg(msg: GetUserApiMsg, actorRef: ActorRef): Unit = { private def handleGetUserApiMsg(msg: GetUserApiMsg, actorRef: ActorRef): Unit = {
log.debug("RECEIVED GetUserApiMsg msg {}", msg) log.debug("RECEIVED GetUserApiMsg msg {}", msg)
RunningMeetings.findWithId(meetings, msg.meetingId) match { sessionTokens.get(msg.sessionToken) match {
case Some(m) => case Some(sessionTokenInfo) =>
m.actorRef forward (msg) 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 => case None =>
actorRef ! ApiResponseFailure("Meeting not found!") actorRef ! ApiResponseFailure("Meeting not found!")
} }
@ -125,6 +154,10 @@ class BigBlueButtonActor(
m <- RunningMeetings.findWithId(meetings, msg.header.meetingId) m <- RunningMeetings.findWithId(meetings, msg.header.meetingId)
} yield { } yield {
log.debug("FORWARDING Register user message") 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) m.actorRef forward (msg)
} }
} }
@ -209,11 +242,15 @@ class BigBlueButtonActor(
context.stop(m.actorRef) context.stop(m.actorRef)
} }
// MeetingDAO.delete(msg.meetingId) //Delay removal of session tokens and Graphql data once users might request some info after the meeting is ended
// MeetingDAO.setMeetingEnded(msg.meetingId) context.system.scheduler.scheduleOnce(Duration.create(60, TimeUnit.MINUTES)) {
// Removing the meeting is enough, all other tables has "ON DELETE CASCADE" log.debug("Removing Graphql data and session tokens. meetingID={}", msg.meetingId)
// UserDAO.softDeleteAllFromMeeting(msg.meetingId)
// MeetingRecordingDAO.updateStopped(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 //Remove ColorPicker idx of the meeting
ColorPicker.reset(m.props.meetingProp.intId) ColorPicker.reset(m.props.meetingProp.intId)

View File

@ -141,7 +141,7 @@ case class UserEstablishedGraphqlConnectionInternalMsg(userId: String, clientTyp
/** /**
* API endpoint /userInfo to provide User Session Variables messages * 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]) case class UserInfosApiMsg(infos: Map[String, Any])
trait ApiResponse trait ApiResponse

View File

@ -2,7 +2,7 @@ package org.bigbluebutton.core.apps.users
import org.apache.pekko.actor.ActorRef import org.apache.pekko.actor.ActorRef
import org.bigbluebutton.core.api.{ ApiResponseFailure, ApiResponseSuccess, GetUserApiMsg, UserInfosApiMsg } 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.core.running.{ HandlerHelpers, LiveMeeting, OutMsgRouter }
import org.bigbluebutton.core2.MeetingStatus2x import org.bigbluebutton.core2.MeetingStatus2x
@ -13,52 +13,52 @@ trait GetUserApiMsgHdlr extends HandlerHelpers {
val outGW: OutMsgRouter val outGW: OutMsgRouter
def handleGetUsersMeetingReqMsg(msg: GetUserApiMsg, actorRef: ActorRef): Unit = { def handleGetUsersMeetingReqMsg(msg: GetUserApiMsg, actorRef: ActorRef): Unit = {
RegisteredUsers.findWithSessionToken(msg.sessionToken, liveMeeting.registeredUsers) match {
val userOption = RegisteredUsers.findWithUserId(msg.userIntId, liveMeeting.registeredUsers)
userOption match {
case Some(regUser) => case Some(regUser) =>
log.debug("replying GetUserApiMsg with success") log.debug("replying GetUserApiMsg with success")
actorRef ! ApiResponseSuccess("User found!", UserInfosApiMsg(getUserInfoResponse(regUser)))
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))
case None => case None =>
log.debug("User not found, sending failure message") log.debug("User not found, sending failure message")
actorRef ! ApiResponseFailure("User not found", Map()) 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
}
} }

View File

@ -30,6 +30,7 @@ trait RegisterUserReqMsgHdlr {
if (liveMeeting.props.usersProp.maxUserConcurrentAccesses > 0) { if (liveMeeting.props.usersProp.maxUserConcurrentAccesses > 0) {
val userConcurrentAccesses = RegisteredUsers.findAllWithExternUserId(regUser.externId, liveMeeting.registeredUsers) val userConcurrentAccesses = RegisteredUsers.findAllWithExternUserId(regUser.externId, liveMeeting.registeredUsers)
.filter(u => !u.loggedOut) .filter(u => !u.loggedOut)
.filter(u => !u.ejected)
.sortWith((u1, u2) => u1.registeredOn > u2.registeredOn) //Remove older first .sortWith((u1, u2) => u1.registeredOn > u2.registeredOn) //Remove older first
val userAvailableSlots = liveMeeting.props.usersProp.maxUserConcurrentAccesses - userConcurrentAccesses.length val userAvailableSlots = liveMeeting.props.usersProp.maxUserConcurrentAccesses - userConcurrentAccesses.length

View File

@ -34,6 +34,7 @@ trait UserJoinMeetingReqMsgHdlr extends HandlerHelpers {
val validationResult = for { val validationResult = for {
_ <- checkIfUserGuestStatusIsAllowed(user) _ <- checkIfUserGuestStatusIsAllowed(user)
_ <- checkIfUserIsBanned(user) _ <- checkIfUserIsBanned(user)
_ <- checkIfUserEjected(user)
_ <- checkIfUserLoggedOut(user) _ <- checkIfUserLoggedOut(user)
_ <- validateMaxParticipants(user) _ <- validateMaxParticipants(user)
} yield 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] = { private def checkIfUserLoggedOut(user: RegisteredUser): Either[(String, String), Unit] = {
if (user.loggedOut) { if (user.loggedOut) {
Left(("User had logged out", EjectReasonCode.USER_LOGGED_OUT)) Left(("User had logged out", EjectReasonCode.USER_LOGGED_OUT))

View File

@ -27,6 +27,7 @@ trait ValidateAuthTokenReqMsgHdlr extends HandlerHelpers {
val validationResult = for { val validationResult = for {
_ <- checkIfUserGuestStatusIsAllowed(user) _ <- checkIfUserGuestStatusIsAllowed(user)
_ <- checkIfUserIsBanned(user) _ <- checkIfUserIsBanned(user)
_ <- checkIfUserEjected(user)
_ <- checkIfUserLoggedOut(user) _ <- checkIfUserLoggedOut(user)
_ <- validateMaxParticipants(user) _ <- validateMaxParticipants(user)
} yield 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] = { private def checkIfUserLoggedOut(user: RegisteredUser): Either[(String, String), Unit] = {
if (user.loggedOut) { if (user.loggedOut) {
Left(("User had logged out", EjectReasonCode.USER_LOGGED_OUT)) Left(("User had logged out", EjectReasonCode.USER_LOGGED_OUT))

View File

@ -24,11 +24,12 @@ object RegisteredUsers {
guestStatus, guestStatus,
excludeFromDashboard, excludeFromDashboard,
System.currentTimeMillis(), System.currentTimeMillis(),
0, lastAuthTokenValidatedOn = 0,
false, graphqlConnected = false,
0, graphqlDisconnectedOn = 0,
false, joined = false,
false, ejected = false,
banned = false,
enforceLayout, enforceLayout,
customParameters, customParameters,
loggedOut, loggedOut,
@ -39,6 +40,10 @@ object RegisteredUsers {
users.toVector.find(u => u.authToken == token) 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] = { def findAll(users: RegisteredUsers): Vector[RegisteredUser] = {
users.toVector users.toVector
} }
@ -128,9 +133,10 @@ object RegisteredUsers {
UserDAO.update(u) UserDAO.update(u)
u u
} else { } else {
users.delete(ejectedUser.id) val u = ejectedUser.modify(_.ejected).setTo(true)
UserDAO.softDelete(ejectedUser.meetingId, ejectedUser.id) users.save(u)
ejectedUser
updateUserJoin(users, u, joined = false)
} }
} }
@ -243,6 +249,7 @@ case class RegisteredUser(
graphqlConnected: Boolean, graphqlConnected: Boolean,
graphqlDisconnectedOn: Long, graphqlDisconnectedOn: Long,
joined: Boolean, joined: Boolean,
ejected: Boolean,
banned: Boolean, banned: Boolean,
enforceLayout: String, enforceLayout: String,
customParameters: Map[String,String], customParameters: Map[String,String],

View File

@ -635,7 +635,7 @@ class MeetingActor(
case m: SetCurrentPagePubMsg => case m: SetCurrentPagePubMsg =>
state = presentationPodsApp.handle(m, state, liveMeeting, msgBus) state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
updateUserLastActivity(m.header.userId) 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: RemovePresentationPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
case m: SetPresentationDownloadablePubMsg => 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) case m: PresentationConversionUpdateSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
@ -961,6 +961,7 @@ class MeetingActor(
regUser <- RegisteredUsers.findAll(liveMeeting.registeredUsers) regUser <- RegisteredUsers.findAll(liveMeeting.registeredUsers)
} yield { } yield {
if (!regUser.loggedOut if (!regUser.loggedOut
&& !regUser.ejected
&& regUser.guestStatus == GuestStatus.WAIT && regUser.guestStatus == GuestStatus.WAIT
&& !regUser.graphqlConnected && !regUser.graphqlConnected
&& regUser.graphqlDisconnectedOn != 0) { && regUser.graphqlDisconnectedOn != 0) {

View File

@ -1,16 +1,16 @@
package org.bigbluebutton.service 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.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.ask
import org.apache.pekko.pattern.AskTimeoutException import org.apache.pekko.pattern.AskTimeoutException
import org.apache.pekko.util.Timeout import org.apache.pekko.util.Timeout
import org.bigbluebutton.common2.util.JsonUtil 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.duration.DurationInt
import scala.concurrent.{ ExecutionContextExecutor, Future } import scala.concurrent.{ExecutionContextExecutor, Future}
object UserInfoService { object UserInfoService {
@ -21,8 +21,8 @@ class UserInfoService(system: ActorSystem, bbbActor: ActorRef) {
implicit def executionContext: ExecutionContextExecutor = system.dispatcher implicit def executionContext: ExecutionContextExecutor = system.dispatcher
implicit val timeout: Timeout = 2 seconds implicit val timeout: Timeout = 2 seconds
def getUserInfo(meetingId: String, userIntId: String): Future[ApiResponse] = { def getUserInfo(sessionToken: String): Future[ApiResponse] = {
val future = bbbActor.ask(GetUserApiMsg(meetingId, userIntId)).mapTo[ApiResponse] val future = bbbActor.ask(GetUserApiMsg(sessionToken)).mapTo[ApiResponse]
future.recover { future.recover {
case e: AskTimeoutException => ApiResponseFailure("Request Timeout error") case e: AskTimeoutException => ApiResponseFailure("Request Timeout error")

View File

@ -13,10 +13,10 @@ import (
// sessionVarsHookUrl is the authentication hook URL obtained from an environment variable. // sessionVarsHookUrl is the authentication hook URL obtained from an environment variable.
var sessionVarsHookUrl = os.Getenv("BBB_GRAPHQL_MIDDLEWARE_SESSION_VARS_HOOK_URL") var sessionVarsHookUrl = os.Getenv("BBB_GRAPHQL_MIDDLEWARE_SESSION_VARS_HOOK_URL")
func AkkaAppsGetSessionVariablesFrom(browserConnectionId string, meetingId string, userId string) (map[string]string, error) { func AkkaAppsGetSessionVariablesFrom(browserConnectionId string, sessionToken string) (map[string]string, error) {
logger := log.WithField("_routine", "BBBWebClient").WithField("browserConnectionId", browserConnectionId) logger := log.WithField("_routine", "AkkaAppsClient").WithField("browserConnectionId", browserConnectionId)
logger.Debug("Starting BBBWebClient") logger.Debug("Starting AkkaAppsClient")
defer logger.Debug("Finished BBBWebClient") defer logger.Debug("Finished AkkaAppsClient")
// Create a new HTTP client with a cookie jar. // Create a new HTTP client with a cookie jar.
client := &http.Client{} 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") 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. // 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 { if err != nil {
return nil, err return nil, err
} }
// Execute the HTTP request to obtain user session variables (like X-Hasura-Role) // 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) resp, err := client.Do(req)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -1,12 +1,14 @@
package bbb_web package bbb_web
import ( import (
"encoding/json"
"fmt" "fmt"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/http/cookiejar" "net/http/cookiejar"
"os" "os"
"strings"
) )
// authHookUrl is the authentication hook URL obtained from an environment variable. // 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) // 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") //req.Header.Set("User-Agent", "hasura-graphql-engine")
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
@ -54,27 +57,44 @@ func BBBWebCheckAuthorization(browserConnectionId string, sessionToken string, c
return "", "", err return "", "", err
} }
var respBodyAsString = string(respBody) log.Trace(string(respBody))
if respBodyAsString != "authorized" { var respBodyAsMap map[string]string
return "", "", fmt.Errorf("auth token not authorized") 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 userId string
var meetingId string var meetingId string
//Get userId and meetingId from response Header //Get userId and meetingId from response Header
for key, values := range resp.Header { for key, value := range normalizedResponse {
for _, value := range values { log.Debug("%s: %s\n", key, value)
log.Debug("%s: %s\n", key, value)
if key == "User-Id" { if key == "x-userid" {
userId = value userId = value
} }
if key == "Meeting-Id" { if key == "x-meetingid" {
meetingId = value meetingId = value
}
} }
} }

View File

@ -240,13 +240,12 @@ func invalidateHasuraConnectionForSessionToken(bc *common.BrowserConnection, ses
func refreshUserSessionVariables(browserConnection *common.BrowserConnection) error { func refreshUserSessionVariables(browserConnection *common.BrowserConnection) error {
BrowserConnectionsMutex.RLock() BrowserConnectionsMutex.RLock()
browserUserId := browserConnection.UserId sessionToken := browserConnection.SessionToken
browserMeetingId := browserConnection.MeetingId
browserConnectionId := browserConnection.Id browserConnectionId := browserConnection.Id
BrowserConnectionsMutex.RUnlock() BrowserConnectionsMutex.RUnlock()
// Check authorization // Check authorization
sessionVariables, err := akka_apps.AkkaAppsGetSessionVariablesFrom(browserConnectionId, browserMeetingId, browserUserId) sessionVariables, err := akka_apps.AkkaAppsGetSessionVariablesFrom(browserConnectionId, sessionToken)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
return fmt.Errorf("error on checking sessionToken authorization") return fmt.Errorf("error on checking sessionToken authorization")

View File

@ -272,8 +272,8 @@ type Mutation {
type Mutation { type Mutation {
pluginLearningAnalyticsDashboardSendGenericData( pluginLearningAnalyticsDashboardSendGenericData(
genericDataForLearningAnalyticsDashboard: json!, genericDataForLearningAnalyticsDashboard: json!
pluginName: String!, pluginName: String!
): Boolean ): Boolean
} }

View File

@ -1,23 +1,6 @@
- name: allowed-queries - name: allowed-queries
definition: definition:
queries: queries:
- name: UserCurrent
query: |
query UserCurrent {
user_current {
userId
name
guestStatus
guestStatusDetails {
guestLobbyMessage
positionInWaitingQueue
}
meeting {
name
logoutUrl
}
}
}
- name: clientStartupSettings - name: clientStartupSettings
query: | query: |
query clientStartupSettings { query clientStartupSettings {

View File

@ -16,12 +16,3 @@
- GET - GET
name: clientStartupSettings name: clientStartupSettings
url: clientStartupSettings url: clientStartupSettings
- comment: ""
definition:
query:
collection_name: allowed-queries
query_name: UserCurrent
methods:
- GET
name: UserCurrent
url: usercurrent

View File

@ -76,8 +76,14 @@ const StartupDataFetch: React.FC<StartupDataFetchProps> = ({
}, },
}).then((resp) => resp.json()) }).then((resp) => resp.json())
.then((data) => { .then((data) => {
const url = `${data.response.graphqlApiUrl}/clientStartupSettings/?sessionToken=${sessionToken}`; const url = `${data.response.graphqlApiUrl}/clientStartupSettings`;
fetch(url, { method: 'get', credentials: 'include' }) fetch(url, {
method: 'get',
credentials: 'include',
headers: {
'x-session-token': sessionToken,
},
})
.then((resp) => resp.json()) .then((resp) => resp.json())
.then((data: Response) => { .then((data: Response) => {
const settings = data.meeting_clientSettings[0]; const settings = data.meeting_clientSettings[0];

View File

@ -40,9 +40,14 @@ const SettingsLoader: React.FC = () => {
.then((data) => { .then((data) => {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const sessionToken = urlParams.get('sessionToken'); const sessionToken = urlParams.get('sessionToken');
const clientStartupSettings = `/clientSettings/?sessionToken=${sessionToken}`; const url = new URL(`${data.response.graphqlApiUrl}/clientSettings`);
const url = new URL(`${data.response.graphqlApiUrl}${clientStartupSettings}`); fetch(url, {
fetch(url, { method: 'get', credentials: 'include' }) method: 'get',
credentials: 'include',
headers: {
'x-session-token': sessionToken,
},
})
.then((resp) => resp.json()) .then((resp) => resp.json())
.then((data: Response) => { .then((data: Response) => {
const settings = data?.meeting_clientSettings[0].clientSettingsJson; const settings = data?.meeting_clientSettings[0].clientSettingsJson;

View File

@ -62,72 +62,51 @@ class ConnectionController {
def checkGraphqlAuthorization = { def checkGraphqlAuthorization = {
try { try {
if(!request.getHeader("User-Agent").startsWith('hasura-graphql-engine')) {
throw new Exception("Invalid User Agent")
}
String sessionToken = request.getHeader("x-session-token") String sessionToken = request.getHeader("x-session-token")
UserSession userSession = meetingService.getUserSessionWithSessionToken(sessionToken) UserSession userSession = meetingService.getUserSessionWithSessionToken(sessionToken)
Boolean allowRequestsWithoutSession = meetingService.getAllowRequestsWithoutSession(sessionToken) Boolean isSessionTokenValid = session[sessionToken] != null
Boolean isSessionTokenInvalid = !session[sessionToken] && !allowRequestsWithoutSession
response.addHeader("Cache-Control", "no-cache") response.addHeader("Cache-Control", "no-cache")
if (userSession != null && !isSessionTokenInvalid) { if (userSession != null && isSessionTokenValid) {
Meeting m = meetingService.getMeeting(userSession.meetingID) Meeting m = meetingService.getMeeting(userSession.meetingID)
User u User u
if(m) { if(m) {
u = m.getUserById(userSession.internalUserId) u = m.getUserById(userSession.internalUserId)
} }
Boolean cursorLocked = false response.addHeader("Meeting-Id", userSession.meetingID)
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.setStatus(200) response.setStatus(200)
withFormat { withFormat {
json { json {
def builder = new JsonBuilder() def builder = new JsonBuilder()
builder { builder {
"response" "authorized" "response" "authorized"
"X-Hasura-Role" m && u && !u.hasLeft() ? "bbb_client" : "not_joined_bbb_client" "X-Currently-Online" m && u && !u.hasLeft() ? "true" : "false"
"X-Hasura-ModeratorInMeeting" u && u.isModerator() ? userSession.meetingID : "" "X-Moderator" u && u.isModerator() ? "true" : "false"
"X-Hasura-PresenterInMeeting" u && u.isPresenter() ? userSession.meetingID : "" "X-Presenter" u && u.isPresenter() ? "true" : "false"
"X-Hasura-UserId" userSession.internalUserId "X-UserId" userSession.internalUserId
"X-Hasura-MeetingId" userSession.meetingID "X-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 : ""
} }
render(contentType: "application/json", text: builder.toPrettyString()) render(contentType: "application/json", text: builder.toPrettyString())
} }
} }
} else { } else if(isSessionTokenValid) {
UserSessionBasicData removedUserSession = meetingService.getRemovedUserSessionWithSessionToken(sessionToken) UserSessionBasicData removedUserSession = meetingService.getRemovedUserSessionWithSessionToken(sessionToken)
if(removedUserSession) { if(removedUserSession) {
response.addHeader("Meeting-Id", removedUserSession.meetingId)
response.setStatus(200) response.setStatus(200)
withFormat { withFormat {
json { json {
def builder = new JsonBuilder() def builder = new JsonBuilder()
builder { builder {
"response" "authorized" "response" "authorized"
"X-Hasura-Role" "not_joined_bbb_client" "X-Currently-Online" "false"
"X-Hasura-ModeratorInMeeting" removedUserSession.isModerator() ? removedUserSession.meetingId : "" "X-Moderator" removedUserSession.isModerator() ? "true" : "false"
"X-Hasura-PresenterInMeeting" "" "X-Presenter" "false"
"X-Hasura-UserId" removedUserSession.userId "X-UserId" removedUserSession.userId
"X-Hasura-MeetingId" removedUserSession.meetingId "X-MeetingId" removedUserSession.meetingId
} }
render(contentType: "application/json", text: builder.toPrettyString()) render(contentType: "application/json", text: builder.toPrettyString())
} }
@ -135,6 +114,8 @@ class ConnectionController {
} else { } else {
throw new Exception("Invalid User Session") throw new Exception("Invalid User Session")
} }
} else {
throw new Exception("Invalid sessionToken")
} }
} catch (Exception e) { } catch (Exception e) {
log.debug("Error while authenticating graphql connection: " + e.getMessage()) log.debug("Error while authenticating graphql connection: " + e.getMessage())

View File

@ -4,7 +4,7 @@ BBB_GRAPHQL_MIDDLEWARE_REDIS_ADDRESS=127.0.0.1:6379
BBB_GRAPHQL_MIDDLEWARE_REDIS_PASSWORD= BBB_GRAPHQL_MIDDLEWARE_REDIS_PASSWORD=
BBB_GRAPHQL_MIDDLEWARE_HASURA_WS=ws://127.0.0.1:8085/v1/graphql BBB_GRAPHQL_MIDDLEWARE_HASURA_WS=ws://127.0.0.1:8085/v1/graphql
BBB_GRAPHQL_MIDDLEWARE_MAX_CONN_PER_SECOND=10 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_SESSION_VARS_HOOK_URL=http://127.0.0.1:8901/userInfo
BBB_GRAPHQL_MIDDLEWARE_GRAPHQL_ACTIONS_URL=http://127.0.0.1:8093 BBB_GRAPHQL_MIDDLEWARE_GRAPHQL_ACTIONS_URL=http://127.0.0.1:8093

View File

@ -25,20 +25,9 @@ location /api/rest {
#Set cache system for client settings #Set cache system for client settings
location ~ ^/api/rest/(clientStartupSettings|clientSettings)/ { location ~ ^/api/rest/(clientStartupSettings|clientSettings)/ {
#Store URL sessionToken once it will be injected as a header X-Session-Token auth_request /bigbluebutton/connection/checkGraphqlAuthorization;
set $session_token "";
if ($args ~* "(?:^|&)sessionToken=([^&]+)") {
set $session_token $1;
}
auth_request /bigbluebutton/connection/checkAuthorization;
auth_request_set $meeting_id $sent_http_meeting_id; 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 client_settings_cache;
proxy_cache_key "$uri|$meeting_id"; proxy_cache_key "$uri|$meeting_id";
proxy_cache_use_stale updating; proxy_cache_use_stale updating;

View File

@ -11,5 +11,5 @@ HASURA_GRAPHQL_SERVER_PORT=8085
HASURA_GRAPHQL_ADMIN_SECRET=bigbluebutton HASURA_GRAPHQL_ADMIN_SECRET=bigbluebutton
HASURA_GRAPHQL_ENABLE_TELEMETRY=false HASURA_GRAPHQL_ENABLE_TELEMETRY=false
HASURA_GRAPHQL_WEBSOCKET_KEEPALIVE=10 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 HASURA_BBB_GRAPHQL_ACTIONS_ADAPTER_URL=http://127.0.0.1:8093