diff --git a/backend/tests/helpers/assertion-helpers.ts b/backend/tests/helpers/assertion-helpers.ts index 1c08ae3..a875cd4 100644 --- a/backend/tests/helpers/assertion-helpers.ts +++ b/backend/tests/helpers/assertion-helpers.ts @@ -512,3 +512,39 @@ const getPermissions = (roomId: string, role: ParticipantRole) => { throw new Error(`Unknown role ${role}`); } }; + +export const expectValidRecordingTokenResponse = ( + response: any, + roomId: string, + participantRole: ParticipantRole, + canRetrieveRecordings: boolean, + canDeleteRecordings: boolean +) => { + expect(response.status).toBe(200); + expect(response.body).toHaveProperty('token'); + + const decodedToken = decodeJWTToken(response.body.token); + + expect(decodedToken).toHaveProperty('video', { + room: roomId + }); + expect(decodedToken).toHaveProperty('metadata'); + const metadata = JSON.parse(decodedToken.metadata); + expect(metadata).toHaveProperty('role', participantRole); + expect(metadata).toHaveProperty('recordingPermissions', { + canRetrieveRecordings, + canDeleteRecordings + }); +}; + +const decodeJWTToken = (token: string) => { + const base64Url = token.split('.')[1]; + const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); + const jsonPayload = decodeURIComponent( + atob(base64) + .split('') + .map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)) + .join('') + ); + return JSON.parse(jsonPayload); +}; diff --git a/backend/tests/helpers/request-helpers.ts b/backend/tests/helpers/request-helpers.ts index 0034ff6..12e77e7 100644 --- a/backend/tests/helpers/request-helpers.ts +++ b/backend/tests/helpers/request-helpers.ts @@ -398,8 +398,19 @@ export const generateRecordingToken = async (roomId: string, secret: string) => .post(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/rooms/${roomId}/recording-token`) .send({ secret - }) - .expect(200); + }); + return response; +}; + +/** + * Generates a token for retrieving/deleting recordings from a room and returns the cookie containing the token + */ +export const generateRecordingTokenCookie = async (roomId: string, secret: string) => { + checkAppIsRunning(); + + // Generate the recording token + const response = await generateRecordingToken(roomId, secret); + expect(response.status).toBe(200); // Return the recording token cookie const cookies = response.headers['set-cookie'] as unknown as string[]; diff --git a/backend/tests/integration/api/rooms/generate-recording-token.test.ts b/backend/tests/integration/api/rooms/generate-recording-token.test.ts new file mode 100644 index 0000000..b36716d --- /dev/null +++ b/backend/tests/integration/api/rooms/generate-recording-token.test.ts @@ -0,0 +1,108 @@ +import { afterAll, beforeAll, describe, expect, it } from '@jest/globals'; +import { ParticipantRole } from '../../../../src/typings/ce/participant.js'; +import { MeetRecordingAccess } from '../../../../src/typings/ce/room-preferences.js'; +import { expectValidRecordingTokenResponse } from '../../../helpers/assertion-helpers.js'; +import { + deleteAllRecordings, + deleteAllRooms, + deleteRoom, + generateRecordingToken, + startTestServer, + updateRecordingAccessPreferencesInRoom +} from '../../../helpers/request-helpers.js'; +import { RoomData, setupSingleRoomWithRecording } from '../../../helpers/test-scenarios.js'; + +describe('Room API Tests', () => { + let roomData: RoomData; + + beforeAll(async () => { + startTestServer(); + roomData = await setupSingleRoomWithRecording(true); + }); + + afterAll(async () => { + await deleteAllRecordings(); + await deleteAllRooms(); + }); + + describe('Generate Recording Token Tests', () => { + it('should generate a recording token with canRetrieve and canDelete permissions when using the moderator secret and recording access is admin-moderator', async () => { + await updateRecordingAccessPreferencesInRoom(roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR); + + const response = await generateRecordingToken(roomData.room.roomId, roomData.moderatorSecret); + expectValidRecordingTokenResponse(response, roomData.room.roomId, ParticipantRole.MODERATOR, true, true); + }); + + it('should generate a recording token with canRetrieve and canDelete permissions when using the moderator secret and recording access is admin-moderator-publisher', async () => { + await updateRecordingAccessPreferencesInRoom( + roomData.room.roomId, + MeetRecordingAccess.ADMIN_MODERATOR_PUBLISHER + ); + + const response = await generateRecordingToken(roomData.room.roomId, roomData.moderatorSecret); + expectValidRecordingTokenResponse(response, roomData.room.roomId, ParticipantRole.MODERATOR, true, true); + }); + + it('should generate a recording token with canRetrieve and canDelete permissions when using the moderator secret and recording access is public', async () => { + await updateRecordingAccessPreferencesInRoom(roomData.room.roomId, MeetRecordingAccess.PUBLIC); + + const response = await generateRecordingToken(roomData.room.roomId, roomData.moderatorSecret); + expectValidRecordingTokenResponse(response, roomData.room.roomId, ParticipantRole.MODERATOR, true, true); + }); + + it('should generate a recording token without any permissions when using the publisher secret and recording access is admin-moderator', async () => { + await updateRecordingAccessPreferencesInRoom(roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR); + + const response = await generateRecordingToken(roomData.room.roomId, roomData.publisherSecret); + expectValidRecordingTokenResponse(response, roomData.room.roomId, ParticipantRole.PUBLISHER, false, false); + }); + + it('should generate a recording token with canRetrieve permission but not canDelete when using the publisher secret and recording access is admin-moderator-publisher', async () => { + await updateRecordingAccessPreferencesInRoom( + roomData.room.roomId, + MeetRecordingAccess.ADMIN_MODERATOR_PUBLISHER + ); + + const response = await generateRecordingToken(roomData.room.roomId, roomData.publisherSecret); + expectValidRecordingTokenResponse(response, roomData.room.roomId, ParticipantRole.PUBLISHER, true, false); + }); + + it('should generate a recording token with canRetrieve permission but not canDelete when using the publisher secret and recording access is public', async () => { + await updateRecordingAccessPreferencesInRoom(roomData.room.roomId, MeetRecordingAccess.PUBLIC); + + const response = await generateRecordingToken(roomData.room.roomId, roomData.publisherSecret); + expectValidRecordingTokenResponse(response, roomData.room.roomId, ParticipantRole.PUBLISHER, true, false); + }); + + it('should succeed even if the room is deleted', async () => { + await updateRecordingAccessPreferencesInRoom(roomData.room.roomId, MeetRecordingAccess.PUBLIC); + await deleteRoom(roomData.room.roomId); + + const response = await generateRecordingToken(roomData.room.roomId, roomData.moderatorSecret); + expectValidRecordingTokenResponse(response, roomData.room.roomId, ParticipantRole.MODERATOR, true, true); + + // Recreate the room with recording + roomData = await setupSingleRoomWithRecording(true); + }); + + it('should fail with a 404 error if there are no recordings in the room', async () => { + await deleteAllRecordings(); + + const response = await generateRecordingToken(roomData.room.roomId, roomData.moderatorSecret); + expect(response.status).toBe(404); + + // Recreate the room with recording + roomData = await setupSingleRoomWithRecording(true); + }); + + it('should fail with a 404 error if the room does not exist', async () => { + const response = await generateRecordingToken('non-existent-room-id', roomData.moderatorSecret); + expect(response.status).toBe(404); + }); + + it('should fail with a 400 error if the secret is invalid', async () => { + const response = await generateRecordingToken(roomData.room.roomId, 'invalid-secret'); + expect(response.status).toBe(400); + }); + }); +}); diff --git a/backend/tests/integration/api/security/recording-security.test.ts b/backend/tests/integration/api/security/recording-security.test.ts index 111d639..bec2e3a 100644 --- a/backend/tests/integration/api/security/recording-security.test.ts +++ b/backend/tests/integration/api/security/recording-security.test.ts @@ -9,7 +9,7 @@ import { deleteAllRecordings, deleteAllRooms, disconnectFakeParticipants, - generateRecordingToken, + generateRecordingTokenCookie, loginUserAsRole, startTestServer, stopAllRecordings, @@ -184,7 +184,7 @@ describe('Recording API Security Tests', () => { it('should succeed when recording access is public and participant is publisher', async () => { await updateRecordingAccessPreferencesInRoom(roomData.room.roomId, MeetRecordingAccess.PUBLIC); - const recordingCookie = await generateRecordingToken(roomData.room.roomId, roomData.publisherSecret); + const recordingCookie = await generateRecordingTokenCookie(roomData.room.roomId, roomData.publisherSecret); const response = await request(app).get(RECORDINGS_PATH).set('Cookie', recordingCookie); expect(response.status).toBe(200); @@ -192,7 +192,7 @@ describe('Recording API Security Tests', () => { it('should succeed when recording access is public and participant is moderator', async () => { await updateRecordingAccessPreferencesInRoom(roomData.room.roomId, MeetRecordingAccess.PUBLIC); - const recordingCookie = await generateRecordingToken(roomData.room.roomId, roomData.moderatorSecret); + const recordingCookie = await generateRecordingTokenCookie(roomData.room.roomId, roomData.moderatorSecret); const response = await request(app).get(RECORDINGS_PATH).set('Cookie', recordingCookie); expect(response.status).toBe(200); @@ -210,7 +210,7 @@ describe('Recording API Security Tests', () => { roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR_PUBLISHER ); - const recordingCookie = await generateRecordingToken(roomData.room.roomId, roomData.publisherSecret); + const recordingCookie = await generateRecordingTokenCookie(roomData.room.roomId, roomData.publisherSecret); const response = await request(app).get(RECORDINGS_PATH).set('Cookie', recordingCookie); expect(response.status).toBe(200); @@ -221,7 +221,7 @@ describe('Recording API Security Tests', () => { roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR_PUBLISHER ); - const recordingCookie = await generateRecordingToken(roomData.room.roomId, roomData.moderatorSecret); + const recordingCookie = await generateRecordingTokenCookie(roomData.room.roomId, roomData.moderatorSecret); const response = await request(app).get(RECORDINGS_PATH).set('Cookie', recordingCookie); expect(response.status).toBe(200); @@ -229,7 +229,7 @@ describe('Recording API Security Tests', () => { it('should fail when recording access is admin-moderator and participant is publisher', async () => { await updateRecordingAccessPreferencesInRoom(roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR); - const recordingCookie = await generateRecordingToken(roomData.room.roomId, roomData.publisherSecret); + const recordingCookie = await generateRecordingTokenCookie(roomData.room.roomId, roomData.publisherSecret); const response = await request(app).get(RECORDINGS_PATH).set('Cookie', recordingCookie); expect(response.status).toBe(403); @@ -237,7 +237,7 @@ describe('Recording API Security Tests', () => { it('should succeed when recording access is admin-moderator and participant is moderator', async () => { await updateRecordingAccessPreferencesInRoom(roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR); - const recordingCookie = await generateRecordingToken(roomData.room.roomId, roomData.moderatorSecret); + const recordingCookie = await generateRecordingTokenCookie(roomData.room.roomId, roomData.moderatorSecret); const response = await request(app).get(RECORDINGS_PATH).set('Cookie', recordingCookie); expect(response.status).toBe(200); @@ -272,7 +272,7 @@ describe('Recording API Security Tests', () => { it('should succeed when recording access is public and participant is publisher', async () => { await updateRecordingAccessPreferencesInRoom(roomData.room.roomId, MeetRecordingAccess.PUBLIC); - const recordingCookie = await generateRecordingToken(roomData.room.roomId, roomData.publisherSecret); + const recordingCookie = await generateRecordingTokenCookie(roomData.room.roomId, roomData.publisherSecret); const response = await request(app).get(`${RECORDINGS_PATH}/${recordingId}`).set('Cookie', recordingCookie); expect(response.status).toBe(200); @@ -280,7 +280,7 @@ describe('Recording API Security Tests', () => { it('should succeed when recording access is public and participant is moderator', async () => { await updateRecordingAccessPreferencesInRoom(roomData.room.roomId, MeetRecordingAccess.PUBLIC); - const recordingCookie = await generateRecordingToken(roomData.room.roomId, roomData.moderatorSecret); + const recordingCookie = await generateRecordingTokenCookie(roomData.room.roomId, roomData.moderatorSecret); const response = await request(app).get(`${RECORDINGS_PATH}/${recordingId}`).set('Cookie', recordingCookie); expect(response.status).toBe(200); @@ -298,7 +298,7 @@ describe('Recording API Security Tests', () => { roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR_PUBLISHER ); - const recordingCookie = await generateRecordingToken(roomData.room.roomId, roomData.publisherSecret); + const recordingCookie = await generateRecordingTokenCookie(roomData.room.roomId, roomData.publisherSecret); const response = await request(app).get(`${RECORDINGS_PATH}/${recordingId}`).set('Cookie', recordingCookie); expect(response.status).toBe(200); @@ -309,7 +309,7 @@ describe('Recording API Security Tests', () => { roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR_PUBLISHER ); - const recordingCookie = await generateRecordingToken(roomData.room.roomId, roomData.moderatorSecret); + const recordingCookie = await generateRecordingTokenCookie(roomData.room.roomId, roomData.moderatorSecret); const response = await request(app).get(`${RECORDINGS_PATH}/${recordingId}`).set('Cookie', recordingCookie); expect(response.status).toBe(200); @@ -317,7 +317,7 @@ describe('Recording API Security Tests', () => { it('should fail when recording access is admin-moderator and participant is publisher', async () => { await updateRecordingAccessPreferencesInRoom(roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR); - const recordingCookie = await generateRecordingToken(roomData.room.roomId, roomData.publisherSecret); + const recordingCookie = await generateRecordingTokenCookie(roomData.room.roomId, roomData.publisherSecret); const response = await request(app).get(`${RECORDINGS_PATH}/${recordingId}`).set('Cookie', recordingCookie); expect(response.status).toBe(403); @@ -325,7 +325,7 @@ describe('Recording API Security Tests', () => { it('should succeed when recording access is admin-moderator and participant is moderator', async () => { await updateRecordingAccessPreferencesInRoom(roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR); - const recordingCookie = await generateRecordingToken(roomData.room.roomId, roomData.moderatorSecret); + const recordingCookie = await generateRecordingTokenCookie(roomData.room.roomId, roomData.moderatorSecret); const response = await request(app).get(`${RECORDINGS_PATH}/${recordingId}`).set('Cookie', recordingCookie); expect(response.status).toBe(200); @@ -360,7 +360,7 @@ describe('Recording API Security Tests', () => { it('should fail when recording access is public and participant is publisher', async () => { await updateRecordingAccessPreferencesInRoom(roomData.room.roomId, MeetRecordingAccess.PUBLIC); - const recordingCookie = await generateRecordingToken(roomData.room.roomId, roomData.publisherSecret); + const recordingCookie = await generateRecordingTokenCookie(roomData.room.roomId, roomData.publisherSecret); const response = await request(app) .delete(`${RECORDINGS_PATH}/${recordingId}`) @@ -370,7 +370,7 @@ describe('Recording API Security Tests', () => { it('should succeed when recording access is public and participant is moderator', async () => { await updateRecordingAccessPreferencesInRoom(roomData.room.roomId, MeetRecordingAccess.PUBLIC); - const recordingCookie = await generateRecordingToken(roomData.room.roomId, roomData.moderatorSecret); + const recordingCookie = await generateRecordingTokenCookie(roomData.room.roomId, roomData.moderatorSecret); const response = await request(app) .delete(`${RECORDINGS_PATH}/${recordingId}`) @@ -390,7 +390,7 @@ describe('Recording API Security Tests', () => { roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR_PUBLISHER ); - const recordingCookie = await generateRecordingToken(roomData.room.roomId, roomData.publisherSecret); + const recordingCookie = await generateRecordingTokenCookie(roomData.room.roomId, roomData.publisherSecret); const response = await request(app) .delete(`${RECORDINGS_PATH}/${recordingId}`) @@ -403,7 +403,7 @@ describe('Recording API Security Tests', () => { roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR_PUBLISHER ); - const recordingCookie = await generateRecordingToken(roomData.room.roomId, roomData.moderatorSecret); + const recordingCookie = await generateRecordingTokenCookie(roomData.room.roomId, roomData.moderatorSecret); const response = await request(app) .delete(`${RECORDINGS_PATH}/${recordingId}`) @@ -413,7 +413,7 @@ describe('Recording API Security Tests', () => { it('should fail when recording access is admin-moderator and participant is publisher', async () => { await updateRecordingAccessPreferencesInRoom(roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR); - const recordingCookie = await generateRecordingToken(roomData.room.roomId, roomData.publisherSecret); + const recordingCookie = await generateRecordingTokenCookie(roomData.room.roomId, roomData.publisherSecret); const response = await request(app) .delete(`${RECORDINGS_PATH}/${recordingId}`) @@ -423,7 +423,7 @@ describe('Recording API Security Tests', () => { it('should succeed when recording access is admin-moderator and participant is moderator', async () => { await updateRecordingAccessPreferencesInRoom(roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR); - const recordingCookie = await generateRecordingToken(roomData.room.roomId, roomData.moderatorSecret); + const recordingCookie = await generateRecordingTokenCookie(roomData.room.roomId, roomData.moderatorSecret); const response = await request(app) .delete(`${RECORDINGS_PATH}/${recordingId}`) @@ -497,7 +497,7 @@ describe('Recording API Security Tests', () => { it('should succeed when recording access is public and participant is publisher', async () => { await updateRecordingAccessPreferencesInRoom(roomData.room.roomId, MeetRecordingAccess.PUBLIC); - const recordingCookie = await generateRecordingToken(roomData.room.roomId, roomData.publisherSecret); + const recordingCookie = await generateRecordingTokenCookie(roomData.room.roomId, roomData.publisherSecret); const response = await request(app) .get(`${RECORDINGS_PATH}/${recordingId}/media`) @@ -507,7 +507,7 @@ describe('Recording API Security Tests', () => { it('should succeed when recording access is public and participant is moderator', async () => { await updateRecordingAccessPreferencesInRoom(roomData.room.roomId, MeetRecordingAccess.PUBLIC); - const recordingCookie = await generateRecordingToken(roomData.room.roomId, roomData.moderatorSecret); + const recordingCookie = await generateRecordingTokenCookie(roomData.room.roomId, roomData.moderatorSecret); const response = await request(app) .get(`${RECORDINGS_PATH}/${recordingId}/media`) @@ -527,7 +527,7 @@ describe('Recording API Security Tests', () => { roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR_PUBLISHER ); - const recordingCookie = await generateRecordingToken(roomData.room.roomId, roomData.publisherSecret); + const recordingCookie = await generateRecordingTokenCookie(roomData.room.roomId, roomData.publisherSecret); const response = await request(app) .get(`${RECORDINGS_PATH}/${recordingId}/media`) @@ -540,7 +540,7 @@ describe('Recording API Security Tests', () => { roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR_PUBLISHER ); - const recordingCookie = await generateRecordingToken(roomData.room.roomId, roomData.moderatorSecret); + const recordingCookie = await generateRecordingTokenCookie(roomData.room.roomId, roomData.moderatorSecret); const response = await request(app) .get(`${RECORDINGS_PATH}/${recordingId}/media`) @@ -550,7 +550,7 @@ describe('Recording API Security Tests', () => { it('should fail when recording access is admin-moderator and participant is publisher', async () => { await updateRecordingAccessPreferencesInRoom(roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR); - const recordingCookie = await generateRecordingToken(roomData.room.roomId, roomData.publisherSecret); + const recordingCookie = await generateRecordingTokenCookie(roomData.room.roomId, roomData.publisherSecret); const response = await request(app) .get(`${RECORDINGS_PATH}/${recordingId}/media`) @@ -560,7 +560,7 @@ describe('Recording API Security Tests', () => { it('should succeed when recording access is admin-moderator and participant is moderator', async () => { await updateRecordingAccessPreferencesInRoom(roomData.room.roomId, MeetRecordingAccess.ADMIN_MODERATOR); - const recordingCookie = await generateRecordingToken(roomData.room.roomId, roomData.moderatorSecret); + const recordingCookie = await generateRecordingTokenCookie(roomData.room.roomId, roomData.moderatorSecret); const response = await request(app) .get(`${RECORDINGS_PATH}/${recordingId}/media`)