backend: streamline recording access authorization and improve room filtering logic
This commit is contained in:
parent
1188255210
commit
11f7ac1401
@ -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.
|
||||
*
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user