From f1c59526e002633f50438186279661a31b08d5d6 Mon Sep 17 00:00:00 2001 From: juancarmore Date: Mon, 28 Apr 2025 12:22:40 +0200 Subject: [PATCH] backend: Enhance recording middleware and routes with new permission checks and authentication for media access --- .../src/middlewares/recording.middleware.ts | 124 +++++++++++++++--- backend/src/routes/recording.routes.ts | 17 ++- 2 files changed, 118 insertions(+), 23 deletions(-) diff --git a/backend/src/middlewares/recording.middleware.ts b/backend/src/middlewares/recording.middleware.ts index 209ec47..9e9416d 100644 --- a/backend/src/middlewares/recording.middleware.ts +++ b/backend/src/middlewares/recording.middleware.ts @@ -1,21 +1,10 @@ -import { MeetRoom, OpenViduMeetPermissions } from '@typings-ce'; +import { MeetRecordingAccess, MeetRoom, OpenViduMeetPermissions, RecordingPermissions, UserRole } from '@typings-ce'; import { NextFunction, Request, Response } from 'express'; import { container } from '../config/index.js'; import { RecordingHelper } from '../helpers/index.js'; import { OpenViduMeetError } from '../models/error.model.js'; -import { LoggerService, RoomService } from '../services/index.js'; - -const extractRoomIdFromRequest = (req: Request): string => { - if (req.body.roomId) { - return req.body.roomId as string; - } - - // If roomId is not in the body, check if it's in the params - const recordingId = req.params.recordingId as string; - - const { roomId } = RecordingHelper.extractInfoFromRecordingId(recordingId); - return roomId; -}; +import { LoggerService, MeetStorageService, RoomService } from '../services/index.js'; +import { allowAnonymous, recordingTokenValidator, tokenAndRoleValidator, withAuth } from './auth.middleware.js'; export const withRecordingEnabled = async (req: Request, res: Response, next: NextFunction) => { const logger = container.get(LoggerService); @@ -23,8 +12,7 @@ export const withRecordingEnabled = async (req: Request, res: Response, next: Ne try { const roomId = extractRoomIdFromRequest(req); - - const room: MeetRoom = await roomService.getMeetRoom(roomId); + const room: MeetRoom = await roomService.getMeetRoom(roomId!); if (!room.preferences?.recordingPreferences?.enabled) { logger.debug(`Recording is disabled for room ${roomId}`); @@ -42,7 +30,7 @@ export const withRecordingEnabled = async (req: Request, res: Response, next: Ne } return res.status(500).json({ - message: 'Unexpected error checking recording permissions' + message: 'Unexpected error checking recording preferences' }); } }; @@ -58,7 +46,7 @@ export const withCanRecordPermission = async (req: Request, res: Response, next: const sameRoom = payload.video?.room === roomId; const metadata = JSON.parse(payload.metadata || '{}'); const permissions = metadata.permissions as OpenViduMeetPermissions | undefined; - const canRecord = permissions?.canRecord === true; + const canRecord = permissions?.canRecord; if (!sameRoom || !canRecord) { return res.status(403).json({ message: 'Insufficient permissions to access this resource' }); @@ -66,3 +54,103 @@ export const withCanRecordPermission = async (req: Request, res: Response, next: return next(); }; + +export const withCanRetrieveRecordingsPermission = async (req: Request, res: Response, next: NextFunction) => { + const roomId = extractRoomIdFromRequest(req); + const payload = req.session?.tokenClaims; + + // If there is no token, it is invoked using the API key, the user is admin or + // the user is anonymous and recording access is public. + // In this case, the user is allowed to access the resource + if (!payload) { + return next(); + } + + const sameRoom = roomId ? payload.video?.room === roomId : true; + const metadata = JSON.parse(payload.metadata || '{}'); + const permissions = metadata.permissions as RecordingPermissions | undefined; + const canRetrieveRecordings = permissions?.canRetrieveRecordings; + + if (!sameRoom || !canRetrieveRecordings) { + return res.status(403).json({ message: 'Insufficient permissions to access this resource' }); + } + + return next(); +}; + +export const withCanDeleteRecordingsPermission = async (req: Request, res: Response, next: NextFunction) => { + const roomId = extractRoomIdFromRequest(req); + const payload = req.session?.tokenClaims; + + // 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 (!payload) { + return next(); + } + + const sameRoom = payload.video?.room === roomId; + const metadata = JSON.parse(payload.metadata || '{}'); + const permissions = metadata.permissions as RecordingPermissions | undefined; + const canDeleteRecordings = permissions?.canDeleteRecordings; + + if (!sameRoom || !canDeleteRecordings) { + return res.status(403).json({ message: 'Insufficient permissions to access this resource' }); + } + + return next(); +}; + +/** + * Middleware to configure authentication for retrieving recording media based on recording access. + * + * - Admin and recording token are always allowed + * - If recording access is public, anonymous users are allowed + */ +export const configureRecordingMediaAuth = async (req: Request, res: Response, next: NextFunction) => { + const logger = container.get(LoggerService); + const storageService = container.get(MeetStorageService); + + let recordingAccess: MeetRecordingAccess; + + try { + const roomId = extractRoomIdFromRequest(req); + const room = await storageService.getArchivedRoomMetadata(roomId!); + + if (!room) { + return res.status(404).json({ + message: 'Room metadata associated with the recording not found' + }); + } + + recordingAccess = room.preferences!.recordingPreferences.allowAccessTo; + } catch (error) { + logger.error(`Error checking recording permissions: ${error}`); + return res.status(500).json({ + message: 'Unexpected error checking recording permissions' + }); + } + + const authValidators = [tokenAndRoleValidator(UserRole.ADMIN), recordingTokenValidator]; + + if (recordingAccess === MeetRecordingAccess.PUBLIC) { + authValidators.push(allowAnonymous); + } + + return withAuth(...authValidators)(req, res, next); +}; + +const extractRoomIdFromRequest = (req: Request): string | undefined => { + if (req.body.roomId) { + return req.body.roomId as string; + } + + // If roomId is not in the body, check if it's in the params + const recordingId = req.params.recordingId as string; + + if (!recordingId) { + return undefined; + } + + const { roomId } = RecordingHelper.extractInfoFromRecordingId(recordingId); + return roomId; +}; diff --git a/backend/src/routes/recording.routes.ts b/backend/src/routes/recording.routes.ts index 0072509..67b9f01 100644 --- a/backend/src/routes/recording.routes.ts +++ b/backend/src/routes/recording.routes.ts @@ -4,10 +4,14 @@ import { Router } from 'express'; import * as recordingCtrl from '../controllers/recording.controller.js'; import { apiKeyValidator, + configureRecordingMediaAuth, participantTokenValidator, + recordingTokenValidator, tokenAndRoleValidator, withAuth, + withCanDeleteRecordingsPermission, withCanRecordPermission, + withCanRetrieveRecordingsPermission, withRecordingEnabled, withValidRecordingBulkDeleteRequest, withValidRecordingFiltersRequest, @@ -22,19 +26,22 @@ recordingRouter.use(bodyParser.json()); // Recording Routes recordingRouter.delete( '/:recordingId', - withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)), + withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN), recordingTokenValidator), withValidRecordingId, + withCanDeleteRecordingsPermission, recordingCtrl.deleteRecording ); recordingRouter.get( '/:recordingId', - withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)), + withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN), recordingTokenValidator), withValidRecordingId, + withCanRetrieveRecordingsPermission, recordingCtrl.getRecording ); recordingRouter.get( '/', - withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)), + withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN), recordingTokenValidator), + withCanRetrieveRecordingsPermission, withValidRecordingFiltersRequest, recordingCtrl.getRecordings ); @@ -44,11 +51,11 @@ recordingRouter.delete( withValidRecordingBulkDeleteRequest, recordingCtrl.bulkDeleteRecordings ); - recordingRouter.get( '/:recordingId/media', - withAuth(tokenAndRoleValidator(UserRole.ADMIN)), + configureRecordingMediaAuth, withValidRecordingId, + withCanRetrieveRecordingsPermission, recordingCtrl.getRecordingMedia );