backend: remove public option from MeetRecordingAccess and refactor related code

This commit is contained in:
juancarmore 2025-06-13 17:18:18 +02:00
parent 0acf064976
commit acd9a4c880
7 changed files with 42 additions and 48 deletions

View File

@ -1,4 +1,4 @@
import { MeetRecordingAccess, MeetRoom, OpenViduMeetPermissions, RecordingPermissions, UserRole } from '@typings-ce'; import { MeetRoom, OpenViduMeetPermissions, RecordingPermissions, UserRole } from '@typings-ce';
import { NextFunction, Request, Response } from 'express'; import { NextFunction, Request, Response } from 'express';
import { container } from '../config/index.js'; import { container } from '../config/index.js';
import { RecordingHelper } from '../helpers/index.js'; import { RecordingHelper } from '../helpers/index.js';
@ -7,7 +7,6 @@ import {
errorInvalidRecordingSecret, errorInvalidRecordingSecret,
errorRecordingDisabled, errorRecordingDisabled,
errorRecordingNotFound, errorRecordingNotFound,
errorRoomMetadataNotFound,
handleError, handleError,
rejectRequestFromMeetError rejectRequestFromMeetError
} from '../models/error.model.js'; } from '../models/error.model.js';
@ -68,10 +67,9 @@ export const withCanRetrieveRecordingsPermission = async (req: Request, res: Res
/** /**
* If there is no token, the user is allowed to access the resource because one of the following reasons: * 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 request is invoked using the API key.
* - The user is admin. * - The user is admin.
* - The user is anonymous and recording access is public.
* - The user is anonymous and is using the public access secret. * - The user is anonymous and is using the public access secret.
* - The user is using the private access secret and is authenticated. * - The user is using the private access secret and is authenticated.
*/ */
@ -116,13 +114,12 @@ export const withCanDeleteRecordingsPermission = async (req: Request, res: Respo
}; };
/** /**
* Middleware to configure authentication for retrieving recording media based on recording access. * Middleware to configure authentication for retrieving recording based on the provided secret.
* *
* - API key, admin and recording token are always allowed.
* - If a valid secret is provided in the query, access is granted according to the secret type. * - If a valid secret is provided in the query, access is granted according to the secret type.
* - If recording access is public, anonymous users are allowed. * - If no secret is provided, the default authentication logic is applied, i.e., API key, admin and recording token access.
*/ */
export const configureRecordingMediaAuth = async (req: Request, res: Response, next: NextFunction) => { export const configureRecordingAuth = async (req: Request, res: Response, next: NextFunction) => {
const storageService = container.get(MeetStorageService); const storageService = container.get(MeetStorageService);
const secret = req.query.secret as string; const secret = req.query.secret as string;
@ -143,9 +140,11 @@ export const configureRecordingMediaAuth = async (req: Request, res: Response, n
switch (secret) { switch (secret) {
case recordingSecrets.publicAccessSecret: case recordingSecrets.publicAccessSecret:
// Public access secret allows anonymous access
authValidators.push(allowAnonymous); authValidators.push(allowAnonymous);
break; break;
case recordingSecrets.privateAccessSecret: case recordingSecrets.privateAccessSecret:
// Private access secret requires authentication with user role
authValidators.push(tokenAndRoleValidator(UserRole.USER)); authValidators.push(tokenAndRoleValidator(UserRole.USER));
break; break;
default: default:
@ -159,31 +158,9 @@ export const configureRecordingMediaAuth = async (req: Request, res: Response, n
} }
} }
// If no secret is provided, determine access based on room's recording preferences // If no secret is provided, we proceed with the default authentication logic.
let recordingAccess: MeetRecordingAccess | undefined; // This will allow API key, admin and recording token access.
try {
const roomId = extractRoomIdFromRequest(req);
const room = await storageService.getArchivedRoomMetadata(roomId!);
if (!room) {
const error = errorRoomMetadataNotFound(roomId!);
return rejectRequestFromMeetError(res, error);
}
recordingAccess = room.preferences!.recordingPreferences.allowAccessTo;
} catch (error) {
return handleError(res, error, 'checking recording permissions');
}
// Always allow API key, admin, and recording token
const authValidators = [apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN), recordingTokenValidator]; const authValidators = [apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN), recordingTokenValidator];
// If access is public, allow anonymous users as well
if (recordingAccess === MeetRecordingAccess.PUBLIC) {
authValidators.push(allowAnonymous);
}
return withAuth(...authValidators)(req, res, next); return withAuth(...authValidators)(req, res, next);
}; };

View File

@ -56,7 +56,12 @@ const StartRecordingRequestSchema = z.object({
}); });
const GetRecordingSchema = z.object({ const GetRecordingSchema = z.object({
recordingId: nonEmptySanitizedRecordingId('recordingId') params: z.object({
recordingId: nonEmptySanitizedRecordingId('recordingId')
}),
query: z.object({
secret: z.string().optional()
})
}); });
const BulkDeleteRecordingsSchema = z.object({ const BulkDeleteRecordingsSchema = z.object({
@ -143,13 +148,28 @@ export const withValidStartRecordingRequest = (req: Request, res: Response, next
}; };
export const withValidRecordingId = (req: Request, res: Response, next: NextFunction) => { export const withValidRecordingId = (req: Request, res: Response, next: NextFunction) => {
const { success, error, data } = GetRecordingSchema.safeParse({ recordingId: req.params.recordingId }); const { success, error, data } = nonEmptySanitizedRecordingId('recordingId').safeParse(req.params.recordingId);
if (!success) {
error.errors[0].path = ['recordingId'];
return rejectUnprocessableRequest(res, error);
}
req.params.recordingId = data;
next();
};
export const withValidGetRecordingRequest = (req: Request, res: Response, next: NextFunction) => {
const { success, error, data } = GetRecordingSchema.safeParse({
params: req.params,
query: req.query
});
if (!success) { if (!success) {
return rejectUnprocessableRequest(res, error); return rejectUnprocessableRequest(res, error);
} }
req.params.recordingId = data.recordingId; req.params.recordingId = data.params.recordingId;
next(); next();
}; };

View File

@ -60,8 +60,7 @@ const validForceQueryParam = () =>
const RecordingAccessSchema: z.ZodType<MeetRecordingAccess> = z.enum([ const RecordingAccessSchema: z.ZodType<MeetRecordingAccess> = z.enum([
MeetRecordingAccess.ADMIN, MeetRecordingAccess.ADMIN,
MeetRecordingAccess.ADMIN_MODERATOR, MeetRecordingAccess.ADMIN_MODERATOR,
MeetRecordingAccess.ADMIN_MODERATOR_PUBLISHER, MeetRecordingAccess.ADMIN_MODERATOR_PUBLISHER
MeetRecordingAccess.PUBLIC
]); ]);
const RecordingPreferencesSchema: z.ZodType<MeetRecordingPreferences> = z const RecordingPreferencesSchema: z.ZodType<MeetRecordingPreferences> = z

View File

@ -4,7 +4,7 @@ import { Router } from 'express';
import * as recordingCtrl from '../controllers/recording.controller.js'; import * as recordingCtrl from '../controllers/recording.controller.js';
import { import {
apiKeyValidator, apiKeyValidator,
configureRecordingMediaAuth, configureRecordingAuth,
participantTokenValidator, participantTokenValidator,
recordingTokenValidator, recordingTokenValidator,
tokenAndRoleValidator, tokenAndRoleValidator,
@ -14,6 +14,7 @@ import {
withCanRetrieveRecordingsPermission, withCanRetrieveRecordingsPermission,
withRecordingEnabled, withRecordingEnabled,
withValidGetRecordingMediaRequest, withValidGetRecordingMediaRequest,
withValidGetRecordingRequest,
withValidGetRecordingUrlRequest, withValidGetRecordingUrlRequest,
withValidRecordingBulkDeleteRequest, withValidRecordingBulkDeleteRequest,
withValidRecordingFiltersRequest, withValidRecordingFiltersRequest,
@ -41,8 +42,8 @@ recordingRouter.delete(
); );
recordingRouter.get( recordingRouter.get(
'/:recordingId', '/:recordingId',
withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN), recordingTokenValidator), withValidGetRecordingRequest,
withValidRecordingId, configureRecordingAuth,
withCanRetrieveRecordingsPermission, withCanRetrieveRecordingsPermission,
recordingCtrl.getRecording recordingCtrl.getRecording
); );
@ -56,7 +57,7 @@ recordingRouter.delete(
recordingRouter.get( recordingRouter.get(
'/:recordingId/media', '/:recordingId/media',
withValidGetRecordingMediaRequest, withValidGetRecordingMediaRequest,
configureRecordingMediaAuth, configureRecordingAuth,
withCanRetrieveRecordingsPermission, withCanRetrieveRecordingsPermission,
recordingCtrl.getRecordingMedia recordingCtrl.getRecordingMedia
); );

View File

@ -46,8 +46,8 @@ roomRouter.delete(
roomRouter.get( roomRouter.get(
'/:roomId', '/:roomId',
withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN), participantTokenValidator), withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN), participantTokenValidator),
configureRoomAuthorization,
withValidRoomId, withValidRoomId,
configureRoomAuthorization,
roomCtrl.getRoom roomCtrl.getRoom
); );
roomRouter.delete( roomRouter.delete(

View File

@ -305,12 +305,10 @@ export class RoomService {
/* A participant can retrieve recordings if /* A participant can retrieve recordings if
- they can delete recordings - they can delete recordings
- the recording access is public
- they are a publisher and the recording access includes publishers - they are a publisher and the recording access includes publishers
*/ */
const canRetrieveRecordings = const canRetrieveRecordings =
canDeleteRecordings || canDeleteRecordings ||
recordingAccess === MeetRecordingAccess.PUBLIC ||
(role === ParticipantRole.PUBLISHER && recordingAccess === MeetRecordingAccess.ADMIN_MODERATOR_PUBLISHER); (role === ParticipantRole.PUBLISHER && recordingAccess === MeetRecordingAccess.ADMIN_MODERATOR_PUBLISHER);
return { return {
@ -450,7 +448,7 @@ export class RoomService {
); );
// Prepare rooms for batch update // Prepare rooms for batch update
const roomsToUpdate: {roomId: string; room: MeetRoom}[] = []; const roomsToUpdate: { roomId: string; room: MeetRoom }[] = [];
const successfulRoomIds: string[] = []; const successfulRoomIds: string[] = [];
roomResults.forEach((result, index) => { roomResults.forEach((result, index) => {
@ -459,7 +457,7 @@ export class RoomService {
if (result.status === 'fulfilled' && result.value) { if (result.status === 'fulfilled' && result.value) {
const room = result.value; const room = result.value;
room.markedForDeletion = true; room.markedForDeletion = true;
roomsToUpdate.push({roomId, room}); roomsToUpdate.push({ roomId, room });
successfulRoomIds.push(roomId); successfulRoomIds.push(roomId);
} else { } else {
this.logger.warn( this.logger.warn(
@ -470,7 +468,7 @@ export class RoomService {
// Batch save all updated rooms // Batch save all updated rooms
if (roomsToUpdate.length > 0) { if (roomsToUpdate.length > 0) {
await Promise.allSettled(roomsToUpdate.map(({room}) => this.storageService.saveMeetRoom(room))); await Promise.allSettled(roomsToUpdate.map(({ room }) => this.storageService.saveMeetRoom(room)));
} }
this.logger.info(`Successfully marked ${successfulRoomIds.length} rooms for deletion`); this.logger.info(`Successfully marked ${successfulRoomIds.length} rooms for deletion`);

View File

@ -19,7 +19,6 @@ export const enum MeetRecordingAccess {
ADMIN = 'admin', // Only admins can access the recording ADMIN = 'admin', // Only admins can access the recording
ADMIN_MODERATOR = 'admin-moderator', // Admins and moderators can access ADMIN_MODERATOR = 'admin-moderator', // Admins and moderators can access
ADMIN_MODERATOR_PUBLISHER = 'admin-moderator-publisher', // Admins, moderators and publishers can access ADMIN_MODERATOR_PUBLISHER = 'admin-moderator-publisher', // Admins, moderators and publishers can access
PUBLIC = 'public', // Everyone can access
} }
export interface MeetChatPreferences { export interface MeetChatPreferences {