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.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)
}
}
}
}

View File

@ -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)

View File

@ -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

View File

@ -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
}
}

View File

@ -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

View File

@ -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))

View File

@ -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))

View File

@ -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],

View File

@ -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) {

View File

@ -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")

View File

@ -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

View File

@ -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
}
}

View File

@ -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")

View File

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

View File

@ -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 {

View File

@ -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

View File

@ -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];

View File

@ -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;

View File

@ -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())

View File

@ -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

View File

@ -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;

View File

@ -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