diff --git a/backend/src/controllers/room.controller.ts b/backend/src/controllers/room.controller.ts index df7b82d..982b57d 100644 --- a/backend/src/controllers/room.controller.ts +++ b/backend/src/controllers/room.controller.ts @@ -44,12 +44,13 @@ export const getRoom = async (req: Request, res: Response) => { const { roomId } = req.params; const fields = req.query.fields as string | undefined; + const role = req.session?.participantRole; try { logger.verbose(`Getting room '${roomId}'`); const roomService = container.get(RoomService); - const room = await roomService.getMeetRoom(roomId, fields); + const room = await roomService.getMeetRoom(roomId, fields, role); return res.status(200).json(room); } catch (error) { diff --git a/backend/src/middlewares/auth.middleware.ts b/backend/src/middlewares/auth.middleware.ts index 9e77de8..1587104 100644 --- a/backend/src/middlewares/auth.middleware.ts +++ b/backend/src/middlewares/auth.middleware.ts @@ -94,6 +94,33 @@ export const tokenAndRoleValidator = (role: UserRole) => { // Configure token validator for participant access export const participantTokenValidator = async (req: Request) => { await validateTokenAndSetSession(req, INTERNAL_CONFIG.PARTICIPANT_TOKEN_COOKIE_NAME); + + // Check if the participant role is provided in the request headers + // This is required to distinguish roles when multiple are present in the token + const participantRole = req.headers[INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER]; + const allRoles = [ParticipantRole.MODERATOR, ParticipantRole.PUBLISHER]; + + if (!participantRole || !allRoles.includes(participantRole as ParticipantRole)) { + throw errorWithControl(errorInvalidParticipantRole(), true); + } + + if (!participantRole || !allRoles.includes(participantRole as ParticipantRole)) { + throw errorWithControl(errorInvalidParticipantRole(), true); + } + + // Check that the specified role is present in the token claims + const metadata = JSON.parse(req.session?.tokenClaims?.metadata || '{}'); + const roles = metadata.roles || []; + const hasRole = roles.some( + (r: { role: ParticipantRole; permissions: OpenViduMeetPermissions }) => r.role === participantRole + ); + + if (!hasRole) { + throw errorWithControl(errorInsufficientPermissions(), true); + } + + // Set the participant role in the session + req.session!.participantRole = participantRole as ParticipantRole; }; // Configure token validator for recording access @@ -121,31 +148,6 @@ const validateTokenAndSetSession = async (req: Request, cookieName: string) => { } catch (error) { throw errorWithControl(errorInvalidToken(), true); } - - // If the token is a participant token, set the participant role in the session - if (cookieName === INTERNAL_CONFIG.PARTICIPANT_TOKEN_COOKIE_NAME) { - const participantRole = req.headers[INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER]; - const allRoles = [ParticipantRole.MODERATOR, ParticipantRole.PUBLISHER]; - - // Ensure the participant role is provided and valid - // This is required to distinguish roles when multiple are present in the token - if (!participantRole || !allRoles.includes(participantRole as ParticipantRole)) { - throw errorWithControl(errorInvalidParticipantRole(), true); - } - - // Check that the specified role is present in the token claims - const metadata = JSON.parse(payload.metadata || '{}'); - const roles = metadata.roles || []; - const hasRole = roles.some( - (r: { role: ParticipantRole; permissions: OpenViduMeetPermissions }) => r.role === participantRole - ); - - if (!hasRole) { - throw errorWithControl(errorInsufficientPermissions(), true); - } - - req.session.participantRole = participantRole as ParticipantRole; - } }; // Configure API key validatior diff --git a/backend/src/middlewares/room.middleware.ts b/backend/src/middlewares/room.middleware.ts index b9d1f14..ee24a3f 100644 --- a/backend/src/middlewares/room.middleware.ts +++ b/backend/src/middlewares/room.middleware.ts @@ -15,13 +15,11 @@ import { allowAnonymous, tokenAndRoleValidator, withAuth } from './auth.middlewa * * - 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. - * - If the user is not a moderator, access is denied. - * - If the user is a moderator and belongs to the room, access is granted. + * - Otherwise, the user is allowed to access the room. */ export const configureRoomAuthorization = async (req: Request, res: Response, next: NextFunction) => { const roomId = req.params.roomId as string; const payload = req.session?.tokenClaims; - const role = req.session?.participantRole; // 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 @@ -31,9 +29,8 @@ export const configureRoomAuthorization = async (req: Request, res: Response, ne const sameRoom = payload.video?.room === roomId; - // If the user does not belong to the requested room, - // or the user is not a moderator, access is denied - if (!sameRoom || role !== ParticipantRole.MODERATOR) { + // If the user does not belong to the requested room, access is denied + if (!sameRoom) { const error = errorInsufficientPermissions(); return rejectRequestFromMeetError(res, error); } diff --git a/backend/src/services/room.service.ts b/backend/src/services/room.service.ts index fb26520..2f08f84 100644 --- a/backend/src/services/room.service.ts +++ b/backend/src/services/room.service.ts @@ -185,7 +185,7 @@ export class RoomService { * @param roomId - The name of the room to retrieve. * @returns A promise that resolves to an {@link MeetRoom} object. */ - async getMeetRoom(roomId: string, fields?: string): Promise { + async getMeetRoom(roomId: string, fields?: string, participantRole?: ParticipantRole): Promise { const meetRoom = await this.storageService.getMeetRoom(roomId); if (!meetRoom) { @@ -193,7 +193,14 @@ export class RoomService { throw errorRoomNotFound(roomId); } - return UtilsHelper.filterObjectFields(meetRoom, fields) as MeetRoom; + const filteredRoom = UtilsHelper.filterObjectFields(meetRoom, fields); + + // Remove moderatorRoomUrl if the participant is a publisher to prevent access to moderator links + if (participantRole === ParticipantRole.PUBLISHER) { + delete filteredRoom.moderatorRoomUrl; + } + + return filteredRoom as MeetRoom; } /** diff --git a/backend/tests/integration/api/rooms/get-room.test.ts b/backend/tests/integration/api/rooms/get-room.test.ts index cc6ea63..67c3102 100644 --- a/backend/tests/integration/api/rooms/get-room.test.ts +++ b/backend/tests/integration/api/rooms/get-room.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeAll, describe, expect, it } from '@jest/globals'; import ms from 'ms'; -import { MeetRecordingAccess } from '../../../../src/typings/ce/index.js'; +import { MeetRecordingAccess, ParticipantRole } from '../../../../src/typings/ce/index.js'; import { expectSuccessRoomResponse, expectValidationError, @@ -8,6 +8,7 @@ import { expectValidRoomWithFields } from '../../../helpers/assertion-helpers.js'; import { createRoom, deleteAllRooms, getRoom, startTestServer } from '../../../helpers/request-helpers.js'; +import { setupSingleRoom } from '../../../helpers/test-scenarios.js'; describe('Room API Tests', () => { beforeAll(() => { @@ -95,6 +96,25 @@ describe('Room API Tests', () => { expectSuccessRoomResponse(response, 'deletion-date', validAutoDeletionDate); }); + + it('should retrieve a room without moderatorRoomUrl when participant is publisher', async () => { + const roomData = await setupSingleRoom(); + const response = await getRoom( + roomData.room.roomId, + undefined, + roomData.publisherCookie, + ParticipantRole.PUBLISHER + ); + expect(response.status).toBe(200); + expect(response.body.moderatorRoomUrl).toBeUndefined(); + }); + + it('should return 404 for a non-existent room', async () => { + const fakeRoomId = 'non-existent-room-id'; + const response = await getRoom(fakeRoomId); + expect(response.status).toBe(404); + expect(response.body.message).toBe(`Room '${fakeRoomId}' does not exist`); + }); }); describe('Get Room Validation failures', () => { diff --git a/backend/tests/integration/api/security/room-security.test.ts b/backend/tests/integration/api/security/room-security.test.ts index c94457b..293a3fa 100644 --- a/backend/tests/integration/api/security/room-security.test.ts +++ b/backend/tests/integration/api/security/room-security.test.ts @@ -142,12 +142,12 @@ describe('Room API Security Tests', () => { expect(response.status).toBe(403); }); - it('should fail when participant is publisher', async () => { + it('should succeed when participant is publisher', async () => { const response = await request(app) .get(`${ROOMS_PATH}/${roomData.room.roomId}`) .set('Cookie', roomData.publisherCookie) .set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.PUBLISHER); - expect(response.status).toBe(403); + expect(response.status).toBe(200); }); });