diff --git a/meet-ce/backend/src/helpers/room.helper.ts b/meet-ce/backend/src/helpers/room.helper.ts index ea482d96..4b4c700c 100644 --- a/meet-ce/backend/src/helpers/room.helper.ts +++ b/meet-ce/backend/src/helpers/room.helper.ts @@ -1,5 +1,7 @@ import { MeetRoom, MeetRoomOptions } from '@openvidu-meet/typings'; +import { Request } from 'express'; import { MEET_ENV } from '../environment.js'; +import { RecordingHelper } from './recording.helper.js'; export class MeetRoomHelper { private constructor() { @@ -81,7 +83,8 @@ export class MeetRoomHelper { * - moderatorSecret: The secret extracted from the moderator room URL */ static extractSecretsFromRoom(room: MeetRoom): { speakerSecret: string; moderatorSecret: string } { - const { speakerUrl, moderatorUrl } = room; + const speakerUrl = room.anonymous.speaker.accessUrl; + const moderatorUrl = room.anonymous.moderator.accessUrl; const parsedSpeakerUrl = new URL(speakerUrl); const speakerSecret = parsedSpeakerUrl.searchParams.get('secret') || ''; @@ -105,4 +108,36 @@ export class MeetRoomHelper { return false; } } + + /** + * Extracts the room ID from the request object. + * It checks the following locations in order: + * 1. req.params.roomId + * 2. req.body.roomId + * 3. req.params.recordingId (extracts roomId from it) + * + * @param req - The express request object + * @returns The extracted room ID or undefined if not found + */ + static getRoomIdFromRequest(req: Request): string | undefined { + // 1. Check params + if (req.params.roomId) { + return req.params.roomId; + } + + // 2. Check body + if (req.body.roomId) { + return req.body.roomId; + } + + // 3. Check recordingId in params + const recordingId = req.params.recordingId; + + if (recordingId) { + const { roomId } = RecordingHelper.extractInfoFromRecordingId(recordingId); + return roomId; + } + + return undefined; + } } diff --git a/meet-ce/backend/src/middlewares/participant.middleware.ts b/meet-ce/backend/src/middlewares/participant.middleware.ts deleted file mode 100644 index cb05a05c..00000000 --- a/meet-ce/backend/src/middlewares/participant.middleware.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { AuthMode, MeetRoomMemberRole, MeetRoomMemberTokenOptions, MeetUserRole } from '@openvidu-meet/typings'; -import { NextFunction, Request, Response } from 'express'; -import { container } from '../config/dependency-injector.config.js'; -import { errorInsufficientPermissions, handleError, rejectRequestFromMeetError } from '../models/error.model.js'; -import { GlobalConfigService } from '../services/global-config.service.js'; -import { RequestSessionService } from '../services/request-session.service.js'; -import { RoomMemberService } from '../services/room-member.service.js'; -import { allowAnonymous, tokenAndRoleValidator, withAuth } from './auth.middleware.js'; - -/** - * Middleware to configure authentication for generating token to access room and its resources - * based on room member role and authentication mode. - * - * - If the authentication mode is MODERATORS_ONLY and the room member role is MODERATOR, configure user authentication. - * - If the authentication mode is ALL_USERS, configure user authentication. - * - Otherwise, allow anonymous access. - */ -export const configureRoomMemberTokenAuth = async (req: Request, res: Response, next: NextFunction) => { - const configService = container.get(GlobalConfigService); - const roomMemberService = container.get(RoomMemberService); - - let role: MeetRoomMemberRole; - - try { - const { roomId } = req.params; - const { secret } = req.body as MeetRoomMemberTokenOptions; - role = await roomMemberService.getRoomMemberRoleBySecret(roomId, secret); - } catch (error) { - return handleError(res, error, 'getting room member role by secret'); - } - - let authModeToAccessRoom: AuthMode; - - try { - const securityConfig = await configService.getSecurityConfig(); - authModeToAccessRoom = securityConfig.authentication.authModeToAccessRoom; - } catch (error) { - return handleError(res, error, 'checking authentication config'); - } - - const authValidators = []; - - if (authModeToAccessRoom === AuthMode.NONE) { - authValidators.push(allowAnonymous); - } else { - const isModeratorsOnlyMode = - authModeToAccessRoom === AuthMode.MODERATORS_ONLY && role === MeetRoomMemberRole.MODERATOR; - const isAllUsersMode = authModeToAccessRoom === AuthMode.ALL_USERS; - - if (isModeratorsOnlyMode || isAllUsersMode) { - authValidators.push(tokenAndRoleValidator(MeetUserRole.USER)); - } else { - authValidators.push(allowAnonymous); - } - } - - return withAuth(...authValidators)(req, res, next); -}; - -export const withModeratorPermissions = async (req: Request, res: Response, next: NextFunction) => { - const { roomId } = req.params; - - const requestSessionService = container.get(RequestSessionService); - const tokenRoomId = requestSessionService.getRoomIdFromToken(); - const role = requestSessionService.getRoomMemberRole(); - - if (!tokenRoomId || !role) { - const error = errorInsufficientPermissions(); - return rejectRequestFromMeetError(res, error); - } - - if (tokenRoomId !== roomId || role !== MeetRoomMemberRole.MODERATOR) { - const error = errorInsufficientPermissions(); - return rejectRequestFromMeetError(res, error); - } - - return next(); -}; diff --git a/meet-ce/backend/src/middlewares/recording.middleware.ts b/meet-ce/backend/src/middlewares/recording.middleware.ts index 3d12dcb4..ae25e366 100644 --- a/meet-ce/backend/src/middlewares/recording.middleware.ts +++ b/meet-ce/backend/src/middlewares/recording.middleware.ts @@ -1,7 +1,7 @@ -import { MeetRoom, MeetUserRole } from '@openvidu-meet/typings'; +import { MeetRoomMemberPermissions, MeetUserRole } from '@openvidu-meet/typings'; import { NextFunction, Request, Response } from 'express'; import { container } from '../config/dependency-injector.config.js'; -import { RecordingHelper } from '../helpers/recording.helper.js'; +import { MeetRoomHelper } from '../helpers/room.helper.js'; import { errorInsufficientPermissions, errorInvalidRecordingSecret, @@ -26,8 +26,8 @@ export const withRecordingEnabled = async (req: Request, res: Response, next: Ne const roomService = container.get(RoomService); try { - const roomId = extractRoomIdFromRequest(req); - const room: MeetRoom = await roomService.getMeetRoom(roomId!); + const roomId = MeetRoomHelper.getRoomIdFromRequest(req); + const room = await roomService.getMeetRoom(roomId!); if (!room.config.recording.enabled) { logger.debug(`Recording is disabled for room '${roomId}'`); @@ -41,97 +41,13 @@ export const withRecordingEnabled = async (req: Request, res: Response, next: Ne } }; -export const withCanRecordPermission = async (req: Request, res: Response, next: NextFunction) => { - const roomId = extractRoomIdFromRequest(req); - - const requestSessionService = container.get(RequestSessionService); - const tokenRoomId = requestSessionService.getRoomIdFromToken(); - const permissions = requestSessionService.getRoomMemberMeetPermissions(); - - if (!tokenRoomId || !permissions) { - const error = errorInsufficientPermissions(); - return rejectRequestFromMeetError(res, error); - } - - if (tokenRoomId !== roomId || !permissions.canRecord) { - const error = errorInsufficientPermissions(); - return rejectRequestFromMeetError(res, error); - } - - return next(); -}; - -export const withCanRetrieveRecordingsPermission = async (req: Request, res: Response, next: NextFunction) => { - const roomId = extractRoomIdFromRequest(req); - - const requestSessionService = container.get(RequestSessionService); - const tokenRoomId = requestSessionService.getRoomIdFromToken(); - - /** - * If there is no token, the user is allowed to access the resource because one of the following reasons: - * - * - The request is invoked using the API key. - * - The user is admin. - * - The user is anonymous and is using the public access secret. - * - The user is using the private access secret and is authenticated. - */ - if (!tokenRoomId) { - return next(); - } - - const permissions = requestSessionService.getRoomMemberMeetPermissions(); - - if (!permissions) { - const error = errorInsufficientPermissions(); - return rejectRequestFromMeetError(res, error); - } - - const sameRoom = roomId ? tokenRoomId === roomId : true; - - if (!sameRoom || !permissions.canRetrieveRecordings) { - const error = errorInsufficientPermissions(); - return rejectRequestFromMeetError(res, error); - } - - return next(); -}; - -export const withCanDeleteRecordingsPermission = async (req: Request, res: Response, next: NextFunction) => { - const roomId = extractRoomIdFromRequest(req); - - const requestSessionService = container.get(RequestSessionService); - const tokenRoomId = requestSessionService.getRoomIdFromToken(); - - // If there is no token, the user is admin or it is invoked using the API key - // In this case, the user is allowed to access the resource - if (!tokenRoomId) { - return next(); - } - - const permissions = requestSessionService.getRoomMemberMeetPermissions(); - - if (!permissions) { - const error = errorInsufficientPermissions(); - return rejectRequestFromMeetError(res, error); - } - - const sameRoom = roomId ? tokenRoomId === roomId : true; - - if (!sameRoom || !permissions.canDeleteRecordings) { - const error = errorInsufficientPermissions(); - return rejectRequestFromMeetError(res, error); - } - - return next(); -}; - /** * Middleware to configure authentication for retrieving recording based on the provided secret. * * - If a valid secret is provided in the query, access is granted according to the secret type. * - If no secret is provided, the default authentication logic is applied, i.e., API key, admin and room member token access. */ -export const configureRecordingAuth = async (req: Request, res: Response, next: NextFunction) => { +export const setupRecordingAuthentication = async (req: Request, res: Response, next: NextFunction) => { const secret = req.query.secret as string; // If a secret is provided, validate it against the stored secrets @@ -151,8 +67,10 @@ export const configureRecordingAuth = async (req: Request, res: Response, next: authValidators.push(allowAnonymous); break; case recordingSecrets.privateAccessSecret: - // Private access secret requires authentication with user role - authValidators.push(tokenAndRoleValidator(MeetUserRole.USER)); + // Private access secret requires authentication + authValidators.push( + tokenAndRoleValidator(MeetUserRole.ADMIN, MeetUserRole.USER, MeetUserRole.ROOM_MEMBER) + ); break; default: // Invalid secret provided @@ -166,23 +84,96 @@ export const configureRecordingAuth = async (req: Request, res: Response, next: } // If no secret is provided, we proceed with the default authentication logic. - // This will allow API key, admin and room member token access. - const authValidators = [apiKeyValidator, tokenAndRoleValidator(MeetUserRole.ADMIN), roomMemberTokenValidator]; + // This will allow API key, registered user and room member token access. + const authValidators = [ + apiKeyValidator, + tokenAndRoleValidator(MeetUserRole.ADMIN, MeetUserRole.USER, MeetUserRole.ROOM_MEMBER), + roomMemberTokenValidator + ]; return withAuth(...authValidators)(req, res, next); }; -const extractRoomIdFromRequest = (req: Request): string | undefined => { - if (req.body.roomId) { - return req.body.roomId as string; - } +/** + * Middleware to authorize recording access (retrieval or deletion). + * + * - If a secret is provided in the request query, and allowSecretAccess is true, + * it assumes the secret has been validated and grants access. + * - If a Room Member Token is used, it checks that the token's roomId matches the requested roomId + * and that the member has the required permission. + * - If a registered user is authenticated, it checks their role and whether they are the owner or a member of the room + * with the required permission. + * - If neither a valid token nor an authenticated user is present, it rejects the request. + * + * @param permission - The permission to check (canRetrieveRecordings or canDeleteRecordings). + * @param allowSecretAccess - Whether to allow access based on a valid secret in the query. + */ +export const authorizeRecordingAccess = (permission: keyof MeetRoomMemberPermissions, allowSecretAccess = false) => { + return async (req: Request, res: Response, next: NextFunction) => { + const roomId = MeetRoomHelper.getRoomIdFromRequest(req); + const secret = req.query.secret as string; - // If roomId is not in the body, check if it's in the params - const recordingId = req.params.recordingId as string; + // If allowSecretAccess is true and a secret is provided, + // we assume it has been validated by setupRecordingAuthentication. + if (allowSecretAccess && secret) { + return next(); + } - if (!recordingId) { - return undefined; - } + const requestSessionService = container.get(RequestSessionService); + const roomService = container.get(RoomService); - const { roomId } = RecordingHelper.extractInfoFromRecordingId(recordingId); - return roomId; + const memberRoomId = requestSessionService.getRoomIdFromMember(); + const user = requestSessionService.getAuthenticatedUser(); + + const forbiddenError = errorInsufficientPermissions(); + + // Case 1: Room Member Token + if (memberRoomId) { + const permissions = requestSessionService.getRoomMemberPermissions(); + + if (!permissions) { + return rejectRequestFromMeetError(res, forbiddenError); + } + + const sameRoom = roomId ? memberRoomId === roomId : true; + + if (!sameRoom || !permissions[permission]) { + return rejectRequestFromMeetError(res, forbiddenError); + } + + return next(); + } + + // Case 2: Authenticated User + if (user) { + // If no roomId is specified, we are in a listing/bulk request + // Each recording's room ownership and permissions will be checked individually + if (!roomId) { + return next(); + } + + // Admins can always access + if (user.role === MeetUserRole.ADMIN) { + return next(); + } + + // Check if owner + const isOwner = await roomService.isRoomOwner(roomId, user.userId); + + if (isOwner) { + return next(); + } + + // Check if member with permissions + const member = await roomService.getRoomMember(roomId, user.userId); + + if (member && member.effectivePermissions[permission]) { + return next(); + } + + return rejectRequestFromMeetError(res, forbiddenError); + } + + // Otherwise, reject the request + return rejectRequestFromMeetError(res, forbiddenError); + }; }; diff --git a/meet-ce/backend/src/middlewares/room-member.middleware.ts b/meet-ce/backend/src/middlewares/room-member.middleware.ts new file mode 100644 index 00000000..94c7eeea --- /dev/null +++ b/meet-ce/backend/src/middlewares/room-member.middleware.ts @@ -0,0 +1,103 @@ +import { MeetRoomMemberPermissions, MeetRoomMemberTokenOptions, MeetUserRole } from '@openvidu-meet/typings'; +import { NextFunction, Request, Response } from 'express'; +import { container } from '../config/dependency-injector.config.js'; +import { MeetRoomHelper } from '../helpers/room.helper.js'; +import { errorInsufficientPermissions, rejectRequestFromMeetError } from '../models/error.model.js'; +import { RequestSessionService } from '../services/request-session.service.js'; +import { RoomService } from '../services/room.service.js'; +import { allowAnonymous, AuthValidator, tokenAndRoleValidator, withAuth } from './auth.middleware.js'; + +/** + * Middleware to configure authentication for generating room member tokens. + * + * - If a secret is provided in the request body, anonymous access is allowed. + * - If no secret is provided, the user must be authenticated as ADMIN, USER, or ROOM_MEMBER. + */ +export const setupRoomMemberTokenAuthentication = async (req: Request, res: Response, next: NextFunction) => { + const { secret } = req.body as MeetRoomMemberTokenOptions; + const authValidators: AuthValidator[] = []; + + if (secret) { + authValidators.push(allowAnonymous); + } else { + authValidators.push(tokenAndRoleValidator(MeetUserRole.ADMIN, MeetUserRole.USER, MeetUserRole.ROOM_MEMBER)); + } + + return withAuth(...authValidators)(req, res, next); +}; + +/** + * Middleware to authorize the generation of a room member token. + * + * - If a secret is provided, it checks if it matches a valid room secret (anonymous access) or if it corresponds to a room member. + * - If no secret is provided, it checks if the authenticated user has permissions to access the room (Admin, Owner, or Member). + */ +export const authorizeRoomMemberTokenGeneration = async (req: Request, res: Response, next: NextFunction) => { + const { roomId } = req.params; + const { secret } = req.body as MeetRoomMemberTokenOptions; + + const requestSessionService = container.get(RequestSessionService); + const roomService = container.get(RoomService); + const user = requestSessionService.getAuthenticatedUser(); + + const forbiddenError = errorInsufficientPermissions(); + + // Scenario 1: Secret provided (Anonymous access or Member ID) + if (secret) { + // Check if secret matches any room access URL secret + const isValidSecret = await roomService.isValidRoomSecret(roomId, secret); + + if (isValidSecret) { + return next(); + } + + // Check if secret is a memberId + const isMember = await roomService.isRoomMember(roomId, secret); + + if (isMember) { + return next(); + } + + return rejectRequestFromMeetError(res, forbiddenError); + } + + // Scenario 2: No secret provided (Authenticated User) + if (user) { + const canAccess = await roomService.canUserAccessRoom(roomId, user); + + if (!canAccess) { + return rejectRequestFromMeetError(res, forbiddenError); + } + + return next(); + } + + return rejectRequestFromMeetError(res, forbiddenError); +}; + +/** + * Middleware to check if the room member has a specific permission. + * + * @param permission The permission to check (key of MeetRoomMemberPermissions). + */ +export const withRoomMemberPermission = (permission: keyof MeetRoomMemberPermissions) => { + return async (req: Request, res: Response, next: NextFunction) => { + const roomId = MeetRoomHelper.getRoomIdFromRequest(req); + + const requestSessionService = container.get(RequestSessionService); + const memberRoomId = requestSessionService.getRoomIdFromMember(); + const permissions = requestSessionService.getRoomMemberPermissions(); + + if (!memberRoomId || !permissions) { + const error = errorInsufficientPermissions(); + return rejectRequestFromMeetError(res, error); + } + + if (memberRoomId !== roomId || !permissions[permission]) { + const error = errorInsufficientPermissions(); + return rejectRequestFromMeetError(res, error); + } + + return next(); + }; +}; diff --git a/meet-ce/backend/src/middlewares/room.middleware.ts b/meet-ce/backend/src/middlewares/room.middleware.ts index 6cde412a..03001b69 100644 --- a/meet-ce/backend/src/middlewares/room.middleware.ts +++ b/meet-ce/backend/src/middlewares/room.middleware.ts @@ -2,31 +2,46 @@ import { NextFunction, Request, Response } from 'express'; import { container } from '../config/dependency-injector.config.js'; import { errorInsufficientPermissions, rejectRequestFromMeetError } from '../models/error.model.js'; import { RequestSessionService } from '../services/request-session.service.js'; +import { RoomService } from '../services/room.service.js'; /** - * Middleware that configures authorization for accessing a specific room. + * Middleware to authorize access to a room. * - * - If there is no token in the session, the user is granted access (admin or API key). - * - If the user does not belong to the requested room, access is denied. - * - Otherwise, the user is allowed to access the room. + * - If a Room Member Token is used, it checks that the token's roomId matches the requested roomId. + * - If a registered user is authenticated, it checks their role and whether they are the owner or a member of the room. + * - If neither a valid token nor an authenticated user is present, it rejects the request. */ -export const configureRoomAuthorization = async (req: Request, res: Response, next: NextFunction) => { +export const authorizeRoomAccess = async (req: Request, res: Response, next: NextFunction) => { const roomId = req.params.roomId as string; const requestSessionService = container.get(RequestSessionService); - const tokenRoomId = requestSessionService.getRoomIdFromToken(); + const memberRoomId = requestSessionService.getRoomIdFromMember(); + const user = requestSessionService.getAuthenticatedUser(); + + const forbiddenError = errorInsufficientPermissions(); + + // Room Member Token + if (memberRoomId) { + // Check if the member's roomId matches the requested roomId + if (memberRoomId !== roomId) { + return rejectRequestFromMeetError(res, forbiddenError); + } - // If there is no token, the user is admin or it is invoked using the API key - // In this case, the user is allowed to access the resource - if (!tokenRoomId) { return next(); } - // If the user does not belong to the requested room, access is denied - if (tokenRoomId !== roomId) { - const error = errorInsufficientPermissions(); - return rejectRequestFromMeetError(res, error); + // Registered User + if (user) { + const roomService = container.get(RoomService); + const canAccess = await roomService.canUserAccessRoom(roomId, user); + + if (!canAccess) { + return rejectRequestFromMeetError(res, forbiddenError); + } + + return next(); } - return next(); + // If there is no token and no user, reject the request + return rejectRequestFromMeetError(res, forbiddenError); }; diff --git a/meet-ce/backend/src/routes/meeting.routes.ts b/meet-ce/backend/src/routes/meeting.routes.ts index 1a9731ea..3f06e284 100644 --- a/meet-ce/backend/src/routes/meeting.routes.ts +++ b/meet-ce/backend/src/routes/meeting.routes.ts @@ -2,7 +2,7 @@ import bodyParser from 'body-parser'; import { Router } from 'express'; import * as meetingCtrl from '../controllers/meeting.controller.js'; import { roomMemberTokenValidator, withAuth } from '../middlewares/auth.middleware.js'; -import { withModeratorPermissions } from '../middlewares/room-member.middleware.js'; +import { withRoomMemberPermission } from '../middlewares/room-member.middleware.js'; import { validateUpdateParticipantRoleReq } from '../middlewares/request-validators/meeting-validator.middleware.js'; import { withValidRoomId } from '../middlewares/request-validators/room-validator.middleware.js'; @@ -15,21 +15,21 @@ internalMeetingRouter.delete( '/:roomId', withAuth(roomMemberTokenValidator), withValidRoomId, - withModeratorPermissions, + withRoomMemberPermission('canEndMeeting'), meetingCtrl.endMeeting ); internalMeetingRouter.delete( '/:roomId/participants/:participantIdentity', withAuth(roomMemberTokenValidator), withValidRoomId, - withModeratorPermissions, + withRoomMemberPermission('canKickParticipants'), meetingCtrl.kickParticipantFromMeeting ); internalMeetingRouter.put( '/:roomId/participants/:participantIdentity/role', withAuth(roomMemberTokenValidator), withValidRoomId, - withModeratorPermissions, + withRoomMemberPermission('canMakeModerator'), validateUpdateParticipantRoleReq, meetingCtrl.updateParticipantRole ); diff --git a/meet-ce/backend/src/routes/recording.routes.ts b/meet-ce/backend/src/routes/recording.routes.ts index b38c1ab2..016f1898 100644 --- a/meet-ce/backend/src/routes/recording.routes.ts +++ b/meet-ce/backend/src/routes/recording.routes.ts @@ -9,10 +9,8 @@ import { withAuth } from '../middlewares/auth.middleware.js'; import { - configureRecordingAuth, - withCanDeleteRecordingsPermission, - withCanRecordPermission, - withCanRetrieveRecordingsPermission, + authorizeRecordingAccess, + setupRecordingAuthentication, withRecordingEnabled } from '../middlewares/recording.middleware.js'; import { @@ -24,6 +22,7 @@ import { validateStartRecordingReq, withValidRecordingId } from '../middlewares/request-validators/recording-validator.middleware.js'; +import { withRoomMemberPermission } from '../middlewares/room-member.middleware.js'; export const recordingRouter: Router = Router(); recordingRouter.use(bodyParser.urlencoded({ extended: true })); @@ -37,8 +36,8 @@ recordingRouter.get( tokenAndRoleValidator(MeetUserRole.ADMIN, MeetUserRole.USER, MeetUserRole.ROOM_MEMBER), roomMemberTokenValidator ), - withCanRetrieveRecordingsPermission, validateGetRecordingsReq, + authorizeRecordingAccess('canRetrieveRecordings'), recordingCtrl.getRecordings ); recordingRouter.delete( @@ -49,7 +48,7 @@ recordingRouter.delete( roomMemberTokenValidator ), validateBulkDeleteRecordingsReq, - withCanDeleteRecordingsPermission, + authorizeRecordingAccess('canDeleteRecordings'), recordingCtrl.bulkDeleteRecordings ); recordingRouter.get( @@ -60,14 +59,14 @@ recordingRouter.get( roomMemberTokenValidator ), validateBulkDeleteRecordingsReq, - withCanRetrieveRecordingsPermission, + authorizeRecordingAccess('canRetrieveRecordings'), recordingCtrl.downloadRecordingsZip ); recordingRouter.get( '/:recordingId', validateGetRecordingReq, - configureRecordingAuth, - withCanRetrieveRecordingsPermission, + setupRecordingAuthentication, + authorizeRecordingAccess('canRetrieveRecordings', true), recordingCtrl.getRecording ); recordingRouter.delete( @@ -78,14 +77,14 @@ recordingRouter.delete( roomMemberTokenValidator ), withValidRecordingId, - withCanDeleteRecordingsPermission, + authorizeRecordingAccess('canDeleteRecordings'), recordingCtrl.deleteRecording ); recordingRouter.get( '/:recordingId/media', validateGetRecordingMediaReq, - configureRecordingAuth, - withCanRetrieveRecordingsPermission, + setupRecordingAuthentication, + authorizeRecordingAccess('canRetrieveRecordings', true), recordingCtrl.getRecordingMedia ); recordingRouter.get( @@ -96,7 +95,7 @@ recordingRouter.get( roomMemberTokenValidator ), validateGetRecordingUrlReq, - withCanRetrieveRecordingsPermission, + authorizeRecordingAccess('canRetrieveRecordings'), recordingCtrl.getRecordingUrl ); @@ -110,7 +109,7 @@ internalRecordingRouter.post( validateStartRecordingReq, withRecordingEnabled, withAuth(roomMemberTokenValidator), - withCanRecordPermission, + withRoomMemberPermission('canRecord'), recordingCtrl.startRecording ); internalRecordingRouter.post( @@ -118,6 +117,6 @@ internalRecordingRouter.post( withValidRecordingId, withRecordingEnabled, withAuth(roomMemberTokenValidator), - withCanRecordPermission, + withRoomMemberPermission('canRecord'), recordingCtrl.stopRecording ); diff --git a/meet-ce/backend/src/routes/room.routes.ts b/meet-ce/backend/src/routes/room.routes.ts index 3167a631..bae8c4d9 100644 --- a/meet-ce/backend/src/routes/room.routes.ts +++ b/meet-ce/backend/src/routes/room.routes.ts @@ -9,7 +9,6 @@ import { tokenAndRoleValidator, withAuth } from '../middlewares/auth.middleware.js'; -import { configureRoomMemberTokenAuth } from '../middlewares/room-member.middleware.js'; import { validateBulkDeleteRoomMembersReq, validateCreateRoomMemberReq, @@ -28,7 +27,11 @@ import { validateUpdateRoomStatusReq, withValidRoomId } from '../middlewares/request-validators/room-validator.middleware.js'; -import { configureRoomAuthorization } from '../middlewares/room.middleware.js'; +import { + authorizeRoomMemberTokenGeneration, + setupRoomMemberTokenAuthentication +} from '../middlewares/room-member.middleware.js'; +import { authorizeRoomAccess } from '../middlewares/room.middleware.js'; export const roomRouter: Router = Router(); roomRouter.use(bodyParser.urlencoded({ extended: true })); @@ -62,7 +65,7 @@ roomRouter.get( roomMemberTokenValidator ), withValidRoomId, - configureRoomAuthorization, + authorizeRoomAccess, roomCtrl.getRoom ); roomRouter.delete( @@ -80,7 +83,7 @@ roomRouter.get( roomMemberTokenValidator ), withValidRoomId, - configureRoomAuthorization, + authorizeRoomAccess, roomCtrl.getRoomConfig ); roomRouter.put( @@ -123,7 +126,7 @@ roomRouter.post( ); roomRouter.get( '/:roomId/members', - withAuth(apiKeyValidator, tokenAndRoleValidator(MeetUserRole.ADMIN, MeetUserRole.USER), roomMemberTokenValidator), + withAuth(apiKeyValidator, tokenAndRoleValidator(MeetUserRole.ADMIN, MeetUserRole.USER, MeetUserRole.ROOM_MEMBER)), withValidRoomId, validateGetRoomMembersReq, roomMemberCtrl.getRoomMembers @@ -169,6 +172,7 @@ internalRoomRouter.post( '/:roomId/members/token', withValidRoomId, validateCreateRoomMemberTokenReq, - configureRoomMemberTokenAuth, + setupRoomMemberTokenAuthentication, + authorizeRoomMemberTokenGeneration, roomMemberCtrl.generateRoomMemberToken ); diff --git a/meet-ce/backend/src/services/request-session.service.ts b/meet-ce/backend/src/services/request-session.service.ts index 62ad781d..72d29b40 100644 --- a/meet-ce/backend/src/services/request-session.service.ts +++ b/meet-ce/backend/src/services/request-session.service.ts @@ -2,7 +2,8 @@ import { MeetRoomMemberPermissions, MeetRoomMemberRole, MeetRoomMemberTokenMetadata, - MeetUser + MeetUser, + MeetUserRole } from '@openvidu-meet/typings'; import { AsyncLocalStorage } from 'async_hooks'; import { injectable } from 'inversify'; @@ -75,6 +76,13 @@ export class RequestSessionService { return this.getContext()?.user; } + /** + * Gets the authenticated user's role from the current request context. + */ + getAuthenticatedUserRole(): MeetUserRole | undefined { + return this.getContext()?.user?.role; + } + /** * Sets the room member token metadata (room ID, base role, permissions) * in the current request context. diff --git a/meet-ce/backend/src/services/room.service.ts b/meet-ce/backend/src/services/room.service.ts index 12ea740b..7b716cce 100644 --- a/meet-ce/backend/src/services/room.service.ts +++ b/meet-ce/backend/src/services/room.service.ts @@ -8,9 +8,12 @@ import { MeetRoomDeletionPolicyWithRecordings, MeetRoomDeletionSuccessCode, MeetRoomFilters, + MeetRoomMember, MeetRoomMemberRole, MeetRoomOptions, - MeetRoomStatus + MeetRoomStatus, + MeetUser, + MeetUserRole } from '@openvidu-meet/typings'; import { inject, injectable } from 'inversify'; import { CreateOptions, Room } from 'livekit-server-sdk'; @@ -233,7 +236,7 @@ export class RoomService { } // Remove moderatorUrl if the room member is a speaker to prevent access to moderator links - const role = this.requestSessionService.getRoomMemberRole(); + const role = this.requestSessionService.getRoomMemberBaseRole(); if (role === MeetRoomMemberRole.SPEAKER) { delete (room as Partial).moderatorUrl; @@ -634,4 +637,70 @@ export class RoomService { ); return { successful, failed }; } + + async isRoomOwner(roomId: string, userId: string): Promise { + // TODO: Implement + return false; + } + + async isRoomMember(roomId: string, memberId: string): Promise { + // TODO: Implement + return false; + } + + async getRoomMember(roomId: string, userId: string): Promise { + // TODO: Implement + return null; + } + + async isValidRoomSecret(roomId: string, secret: string): Promise { + // TODO: Implement + return false; + } + + /** + * Checks if a registered user can access a specific room based on their role. + * + * @param roomId The ID of the room to check access for. + * @param user The user object containing user details and role. + * @returns A promise that resolves to true if the user can access the room, false otherwise. + */ + async canUserAccessRoom(roomId: string, user: MeetUser): Promise { + switch (user.role) { + case MeetUserRole.ADMIN: + // Admins can access all rooms + return true; + + case MeetUserRole.USER: { + // Users can access rooms they own or are members of + const isOwner = await this.isRoomOwner(roomId, user.userId); + + if (isOwner) { + return true; + } + + const isMember = await this.isRoomMember(roomId, user.userId); + + if (isMember) { + return true; + } + + return false; + } + + case MeetUserRole.ROOM_MEMBER: { + // Room members can only access rooms they are members of + const isMember = await this.isRoomMember(roomId, user.userId); + + if (isMember) { + return true; + } + + return false; + } + + default: + return false; + } + } }