backend: streamline recording access authorization and improve room filtering logic

This commit is contained in:
juancarmore 2026-01-28 16:06:46 +01:00
parent 1188255210
commit 11f7ac1401
6 changed files with 54 additions and 82 deletions

View File

@ -10,7 +10,6 @@ import {
} from '../models/error.model.js';
import { LoggerService } from '../services/logger.service.js';
import { RecordingService } from '../services/recording.service.js';
import { RequestSessionService } from '../services/request-session.service.js';
import { RoomService } from '../services/room.service.js';
import {
allowAnonymous,
@ -98,21 +97,26 @@ export const setupRecordingAuthentication = async (req: Request, res: Response,
/**
* Middleware to authorize access (retrieval or deletion) for a single recording.
*
* - If a secret is provided in the request query, it is assumed to have been validated already.
* In that case, access is granted directly for retrieval requests.
* - If a valid secret is provided in the query and `allowAccessWithSecret` is true,
* access is granted directly for retrieval requests.
* - If no secret is provided, the recording's existence and permissions are checked
* based on the authenticated context (room member token or registered user).
*
* @param permission - The permission to check (canRetrieveRecordings or canDeleteRecordings).
* @param allowAccessWithSecret - Whether to allow access based on a valid secret in the query.
*/
export const authorizeRecordingAccess = (permission: keyof MeetRoomMemberPermissions) => {
export const authorizeRecordingAccess = (
permission: keyof MeetRoomMemberPermissions,
allowAccessWithSecret = false
) => {
return async (req: Request, res: Response, next: NextFunction) => {
const recordingId = req.params.recordingId as string;
const secret = req.query.secret as string | undefined;
// If a secret is provided, we assume it has been validated by setupRecordingAuthentication.
// If allowAccessWithSecret is true and a secret is provided,
// we assume that the secret has been validated by setupRecordingAuthentication.
// In that case, grant access directly for retrieval requests.
if (secret && permission === 'canRetrieveRecordings') {
if (allowAccessWithSecret && secret && permission === 'canRetrieveRecordings') {
return next();
}
@ -127,38 +131,6 @@ export const authorizeRecordingAccess = (permission: keyof MeetRoomMemberPermiss
};
};
/**
* Middleware to authorize access (retrieval or deletion) for multiple recordings.
*
* - If a room member token is present, checks if the member has the specified permission.
* - If no room member token is present, each recording's permissions will be checked individually later.
*
* @param permission - The permission to check (canRetrieveRecordings or canDeleteRecordings).
*/
export const authorizeBulkRecordingAccess = (permission: keyof MeetRoomMemberPermissions) => {
return async (_req: Request, res: Response, next: NextFunction) => {
const requestSessionService = container.get(RequestSessionService);
const memberRoomId = requestSessionService.getRoomIdFromMember();
// If there is no room member token,
// each recording's permissions will be checked individually later
if (!memberRoomId) {
return next();
}
// If there is a room member token, check permissions now
// because they have the same permissions for all recordings in the room associated with the token
const permissions = requestSessionService.getRoomMemberPermissions();
if (!permissions || !permissions[permission]) {
const forbiddenError = errorInsufficientPermissions();
return rejectRequestFromMeetError(res, forbiddenError);
}
return next();
};
};
/**
* Middleware to authorize control actions (start/stop) for recordings.
*

View File

@ -120,7 +120,7 @@ export class RecordingRepository<TRecording extends MeetRecordingInfo = MeetReco
// Build base filter
const filter: Record<string, unknown> = {};
if (roomIds && roomIds.length > 0) {
if (roomIds) {
// Filter by multiple room IDs
filter.roomId = { $in: roomIds };
}

View File

@ -68,18 +68,6 @@ export class RoomRepository<TRoom extends MeetRoom = MeetRoom> extends BaseRepos
return document ? this.enrichRoomWithBaseUrls(document) : null;
}
/**
* Finds rooms by their roomIds.
* Returns rooms with enriched URLs (including base URL).
*
* @param roomIds - Array of room identifiers
* @param fields - Comma-separated list of fields to include in the result
* @returns Array of found rooms
*/
async findByRoomIds(roomIds: string[], fields?: string): Promise<TRoom[]> {
return await this.findAll({ roomId: { $in: roomIds } }, fields);
}
/**
* Finds rooms owned by a specific user.
* Returns rooms with enriched URLs (including base URL).
@ -132,11 +120,11 @@ export class RoomRepository<TRoom extends MeetRoom = MeetRoom> extends BaseRepos
const filter: Record<string, unknown> = {};
// Handle owner and roomIds with $or when both are present
if (owner && roomIds && roomIds.length > 0) {
if (owner && roomIds) {
filter.$or = [{ owner }, { roomId: { $in: roomIds } }];
} else if (owner) {
filter.owner = owner;
} else if (roomIds && roomIds.length > 0) {
} else if (roomIds) {
filter.roomId = { $in: roomIds };
}

View File

@ -9,7 +9,6 @@ import {
withAuth
} from '../middlewares/auth.middleware.js';
import {
authorizeBulkRecordingAccess,
authorizeRecordingAccess,
authorizeRecordingControl,
setupRecordingAuthentication,
@ -38,7 +37,6 @@ recordingRouter.get(
tokenAndRoleValidator(MeetUserRole.ADMIN, MeetUserRole.USER, MeetUserRole.ROOM_MEMBER)
),
validateGetRecordingsReq,
authorizeBulkRecordingAccess('canRetrieveRecordings'),
recordingCtrl.getRecordings
);
recordingRouter.delete(
@ -49,7 +47,6 @@ recordingRouter.delete(
tokenAndRoleValidator(MeetUserRole.ADMIN, MeetUserRole.USER, MeetUserRole.ROOM_MEMBER)
),
validateBulkDeleteRecordingsReq,
authorizeBulkRecordingAccess('canDeleteRecordings'),
recordingCtrl.bulkDeleteRecordings
);
recordingRouter.get(
@ -60,14 +57,13 @@ recordingRouter.get(
tokenAndRoleValidator(MeetUserRole.ADMIN, MeetUserRole.USER, MeetUserRole.ROOM_MEMBER)
),
validateBulkDeleteRecordingsReq,
authorizeBulkRecordingAccess('canRetrieveRecordings'),
recordingCtrl.downloadRecordingsZip
);
recordingRouter.get(
'/:recordingId',
validateGetRecordingReq,
setupRecordingAuthentication,
authorizeRecordingAccess('canRetrieveRecordings'),
authorizeRecordingAccess('canRetrieveRecordings', true),
recordingCtrl.getRecording
);
recordingRouter.delete(
@ -85,7 +81,7 @@ recordingRouter.get(
'/:recordingId/media',
validateGetRecordingMediaReq,
setupRecordingAuthentication,
authorizeRecordingAccess('canRetrieveRecordings'),
authorizeRecordingAccess('canRetrieveRecordings', true),
recordingCtrl.getRecordingMedia
);
recordingRouter.get(

View File

@ -231,30 +231,24 @@ export class RecordingService {
nextPageToken?: string;
}> {
try {
const memberRoomId = this.requestSessionService.getRoomIdFromMember();
const queryOptions: MeetRecordingFilters & { roomIds?: string[]; owner?: string } = { ...filters };
const queryOptions: MeetRecordingFilters & { roomIds?: string[] } = { ...filters };
// If room member token is present, retrieve only recordings for the room associated with the token
if (memberRoomId) {
queryOptions.roomId = memberRoomId;
} else {
// Get accessible room IDs based on user role and permissions
const roomService = await this.getRoomService();
const accessibleRoomIds = await roomService.getAccessibleRoomIds('canRetrieveRecordings');
// Get accessible room IDs based on authenticated user and their permissions
const roomService = await this.getRoomService();
const accessibleRoomIds = await roomService.getAccessibleRoomIds('canRetrieveRecordings');
if (accessibleRoomIds !== null) {
if (accessibleRoomIds.length === 0) {
// User has no access to any rooms, return empty result
return {
recordings: [],
isTruncated: false
};
}
// Apply roomIds filter
queryOptions.roomIds = accessibleRoomIds;
// If accessibleRoomIds is null, user is ADMIN and no filter is applied
if (accessibleRoomIds !== null) {
if (accessibleRoomIds.length === 0) {
// User has no access to any rooms, return empty result
return {
recordings: [],
isTruncated: false
};
}
// If accessibleRoomIds is null, user is ADMIN and no filter is applied
// Apply roomIds filter
queryOptions.roomIds = accessibleRoomIds;
}
const response = await this.recordingRepository.find(queryOptions);

View File

@ -321,8 +321,8 @@ export class RoomService {
isTruncated: boolean;
nextPageToken?: string;
}> {
const user = this.requestSessionService.getAuthenticatedUser();
const queryOptions: MeetRoomFilters & { roomIds?: string[]; owner?: string } = { ...filters };
const user = this.requestSessionService.getAuthenticatedUser();
// Admin can see all rooms - no additional filters needed
if (user && user.role !== MeetUserRole.ADMIN) {
@ -342,10 +342,32 @@ export class RoomService {
/**
* Gets the list of room IDs accessible by the authenticated user based on their role and permissions.
*
* - If the request is made with a room member token, only that room ID is returned (if permissions allow).
* - If the user is an ADMIN, null is returned indicating access to all rooms.
* - If the user is a USER, room IDs they own and are members of are returned.
* - If the user is a ROOM_MEMBER, only room IDs they are members of are returned.
*
* @param permission - Optional permission to filter rooms (e.g., 'canRetrieveRecordings')
* @returns A promise that resolves to an array of accessible room IDs, or null if user is ADMIN (no filter needed)
* @returns A promise that resolves to an array of accessible room IDs, or null if user is ADMIN
*/
async getAccessibleRoomIds(permission?: keyof MeetRoomMemberPermissions): Promise<string[] | null> {
const memberRoomId = this.requestSessionService.getRoomIdFromMember();
// If request is made with room member token,
// the only accessible room is the one associated with the token
if (memberRoomId) {
// Check permissions from token if specified
if (permission) {
const permissions = this.requestSessionService.getRoomMemberPermissions();
if (!permissions || !permissions[permission]) {
return [];
}
}
return [memberRoomId];
}
const user = this.requestSessionService.getAuthenticatedUser();
// Admin has access to all rooms