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 { container } from '../config/index.js';
import { RecordingHelper } from '../helpers/index.js';
@ -7,7 +7,6 @@ import {
errorInvalidRecordingSecret,
errorRecordingDisabled,
errorRecordingNotFound,
errorRoomMetadataNotFound,
handleError,
rejectRequestFromMeetError
} 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:
*
*
* - The request is invoked using the API key.
* - 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 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 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 secret = req.query.secret as string;
@ -143,9 +140,11 @@ export const configureRecordingMediaAuth = async (req: Request, res: Response, n
switch (secret) {
case recordingSecrets.publicAccessSecret:
// Public access secret allows anonymous access
authValidators.push(allowAnonymous);
break;
case recordingSecrets.privateAccessSecret:
// Private access secret requires authentication with user role
authValidators.push(tokenAndRoleValidator(UserRole.USER));
break;
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
let recordingAccess: MeetRecordingAccess | undefined;
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
// If no secret is provided, we proceed with the default authentication logic.
// This will allow API key, admin and recording token access.
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);
};

View File

@ -56,7 +56,12 @@ const StartRecordingRequestSchema = 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({
@ -143,13 +148,28 @@ export const withValidStartRecordingRequest = (req: Request, res: Response, next
};
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) {
return rejectUnprocessableRequest(res, error);
}
req.params.recordingId = data.recordingId;
req.params.recordingId = data.params.recordingId;
next();
};

View File

@ -60,8 +60,7 @@ const validForceQueryParam = () =>
const RecordingAccessSchema: z.ZodType<MeetRecordingAccess> = z.enum([
MeetRecordingAccess.ADMIN,
MeetRecordingAccess.ADMIN_MODERATOR,
MeetRecordingAccess.ADMIN_MODERATOR_PUBLISHER,
MeetRecordingAccess.PUBLIC
MeetRecordingAccess.ADMIN_MODERATOR_PUBLISHER
]);
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 {
apiKeyValidator,
configureRecordingMediaAuth,
configureRecordingAuth,
participantTokenValidator,
recordingTokenValidator,
tokenAndRoleValidator,
@ -14,6 +14,7 @@ import {
withCanRetrieveRecordingsPermission,
withRecordingEnabled,
withValidGetRecordingMediaRequest,
withValidGetRecordingRequest,
withValidGetRecordingUrlRequest,
withValidRecordingBulkDeleteRequest,
withValidRecordingFiltersRequest,
@ -41,8 +42,8 @@ recordingRouter.delete(
);
recordingRouter.get(
'/:recordingId',
withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN), recordingTokenValidator),
withValidRecordingId,
withValidGetRecordingRequest,
configureRecordingAuth,
withCanRetrieveRecordingsPermission,
recordingCtrl.getRecording
);
@ -56,7 +57,7 @@ recordingRouter.delete(
recordingRouter.get(
'/:recordingId/media',
withValidGetRecordingMediaRequest,
configureRecordingMediaAuth,
configureRecordingAuth,
withCanRetrieveRecordingsPermission,
recordingCtrl.getRecordingMedia
);

View File

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

View File

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

View File

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