Merge pull request #20686 from gustavotrott/gql-server-auth-session
refactor (graphql): Big refactor on Graphql authentication
This commit is contained in:
commit
7a03136d02
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
@ -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))
|
||||
|
@ -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],
|
||||
|
@ -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) {
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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")
|
||||
|
@ -272,8 +272,8 @@ type Mutation {
|
||||
|
||||
type Mutation {
|
||||
pluginLearningAnalyticsDashboardSendGenericData(
|
||||
genericDataForLearningAnalyticsDashboard: json!,
|
||||
pluginName: String!,
|
||||
genericDataForLearningAnalyticsDashboard: json!
|
||||
pluginName: String!
|
||||
): Boolean
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -76,8 +76,14 @@ const StartupDataFetch: React.FC<StartupDataFetchProps> = ({
|
||||
},
|
||||
}).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];
|
||||
|
@ -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;
|
||||
|
@ -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())
|
||||
|
@ -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
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user