backend: Enhance recording middleware and routes with new permission checks and authentication for media access

This commit is contained in:
juancarmore 2025-04-28 12:22:40 +02:00
parent fd878cd3ea
commit f1c59526e0
2 changed files with 118 additions and 23 deletions

View File

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

View File

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