backend: refactor room member and user authentication middleware, adding new permissions and request handling
This commit is contained in:
parent
b709ad320a
commit
79cef519b8
@ -1,5 +1,7 @@
|
||||
import { MeetRoom, MeetRoomOptions } from '@openvidu-meet/typings';
|
||||
import { Request } from 'express';
|
||||
import { MEET_ENV } from '../environment.js';
|
||||
import { RecordingHelper } from './recording.helper.js';
|
||||
|
||||
export class MeetRoomHelper {
|
||||
private constructor() {
|
||||
@ -81,7 +83,8 @@ export class MeetRoomHelper {
|
||||
* - moderatorSecret: The secret extracted from the moderator room URL
|
||||
*/
|
||||
static extractSecretsFromRoom(room: MeetRoom): { speakerSecret: string; moderatorSecret: string } {
|
||||
const { speakerUrl, moderatorUrl } = room;
|
||||
const speakerUrl = room.anonymous.speaker.accessUrl;
|
||||
const moderatorUrl = room.anonymous.moderator.accessUrl;
|
||||
|
||||
const parsedSpeakerUrl = new URL(speakerUrl);
|
||||
const speakerSecret = parsedSpeakerUrl.searchParams.get('secret') || '';
|
||||
@ -105,4 +108,36 @@ export class MeetRoomHelper {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the room ID from the request object.
|
||||
* It checks the following locations in order:
|
||||
* 1. req.params.roomId
|
||||
* 2. req.body.roomId
|
||||
* 3. req.params.recordingId (extracts roomId from it)
|
||||
*
|
||||
* @param req - The express request object
|
||||
* @returns The extracted room ID or undefined if not found
|
||||
*/
|
||||
static getRoomIdFromRequest(req: Request): string | undefined {
|
||||
// 1. Check params
|
||||
if (req.params.roomId) {
|
||||
return req.params.roomId;
|
||||
}
|
||||
|
||||
// 2. Check body
|
||||
if (req.body.roomId) {
|
||||
return req.body.roomId;
|
||||
}
|
||||
|
||||
// 3. Check recordingId in params
|
||||
const recordingId = req.params.recordingId;
|
||||
|
||||
if (recordingId) {
|
||||
const { roomId } = RecordingHelper.extractInfoFromRecordingId(recordingId);
|
||||
return roomId;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,78 +0,0 @@
|
||||
import { AuthMode, MeetRoomMemberRole, MeetRoomMemberTokenOptions, MeetUserRole } from '@openvidu-meet/typings';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import { container } from '../config/dependency-injector.config.js';
|
||||
import { errorInsufficientPermissions, handleError, rejectRequestFromMeetError } from '../models/error.model.js';
|
||||
import { GlobalConfigService } from '../services/global-config.service.js';
|
||||
import { RequestSessionService } from '../services/request-session.service.js';
|
||||
import { RoomMemberService } from '../services/room-member.service.js';
|
||||
import { allowAnonymous, tokenAndRoleValidator, withAuth } from './auth.middleware.js';
|
||||
|
||||
/**
|
||||
* Middleware to configure authentication for generating token to access room and its resources
|
||||
* based on room member role and authentication mode.
|
||||
*
|
||||
* - If the authentication mode is MODERATORS_ONLY and the room member role is MODERATOR, configure user authentication.
|
||||
* - If the authentication mode is ALL_USERS, configure user authentication.
|
||||
* - Otherwise, allow anonymous access.
|
||||
*/
|
||||
export const configureRoomMemberTokenAuth = async (req: Request, res: Response, next: NextFunction) => {
|
||||
const configService = container.get(GlobalConfigService);
|
||||
const roomMemberService = container.get(RoomMemberService);
|
||||
|
||||
let role: MeetRoomMemberRole;
|
||||
|
||||
try {
|
||||
const { roomId } = req.params;
|
||||
const { secret } = req.body as MeetRoomMemberTokenOptions;
|
||||
role = await roomMemberService.getRoomMemberRoleBySecret(roomId, secret);
|
||||
} catch (error) {
|
||||
return handleError(res, error, 'getting room member role by secret');
|
||||
}
|
||||
|
||||
let authModeToAccessRoom: AuthMode;
|
||||
|
||||
try {
|
||||
const securityConfig = await configService.getSecurityConfig();
|
||||
authModeToAccessRoom = securityConfig.authentication.authModeToAccessRoom;
|
||||
} catch (error) {
|
||||
return handleError(res, error, 'checking authentication config');
|
||||
}
|
||||
|
||||
const authValidators = [];
|
||||
|
||||
if (authModeToAccessRoom === AuthMode.NONE) {
|
||||
authValidators.push(allowAnonymous);
|
||||
} else {
|
||||
const isModeratorsOnlyMode =
|
||||
authModeToAccessRoom === AuthMode.MODERATORS_ONLY && role === MeetRoomMemberRole.MODERATOR;
|
||||
const isAllUsersMode = authModeToAccessRoom === AuthMode.ALL_USERS;
|
||||
|
||||
if (isModeratorsOnlyMode || isAllUsersMode) {
|
||||
authValidators.push(tokenAndRoleValidator(MeetUserRole.USER));
|
||||
} else {
|
||||
authValidators.push(allowAnonymous);
|
||||
}
|
||||
}
|
||||
|
||||
return withAuth(...authValidators)(req, res, next);
|
||||
};
|
||||
|
||||
export const withModeratorPermissions = async (req: Request, res: Response, next: NextFunction) => {
|
||||
const { roomId } = req.params;
|
||||
|
||||
const requestSessionService = container.get(RequestSessionService);
|
||||
const tokenRoomId = requestSessionService.getRoomIdFromToken();
|
||||
const role = requestSessionService.getRoomMemberRole();
|
||||
|
||||
if (!tokenRoomId || !role) {
|
||||
const error = errorInsufficientPermissions();
|
||||
return rejectRequestFromMeetError(res, error);
|
||||
}
|
||||
|
||||
if (tokenRoomId !== roomId || role !== MeetRoomMemberRole.MODERATOR) {
|
||||
const error = errorInsufficientPermissions();
|
||||
return rejectRequestFromMeetError(res, error);
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
@ -1,7 +1,7 @@
|
||||
import { MeetRoom, MeetUserRole } from '@openvidu-meet/typings';
|
||||
import { MeetRoomMemberPermissions, MeetUserRole } from '@openvidu-meet/typings';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import { container } from '../config/dependency-injector.config.js';
|
||||
import { RecordingHelper } from '../helpers/recording.helper.js';
|
||||
import { MeetRoomHelper } from '../helpers/room.helper.js';
|
||||
import {
|
||||
errorInsufficientPermissions,
|
||||
errorInvalidRecordingSecret,
|
||||
@ -26,8 +26,8 @@ export const withRecordingEnabled = async (req: Request, res: Response, next: Ne
|
||||
const roomService = container.get(RoomService);
|
||||
|
||||
try {
|
||||
const roomId = extractRoomIdFromRequest(req);
|
||||
const room: MeetRoom = await roomService.getMeetRoom(roomId!);
|
||||
const roomId = MeetRoomHelper.getRoomIdFromRequest(req);
|
||||
const room = await roomService.getMeetRoom(roomId!);
|
||||
|
||||
if (!room.config.recording.enabled) {
|
||||
logger.debug(`Recording is disabled for room '${roomId}'`);
|
||||
@ -41,97 +41,13 @@ export const withRecordingEnabled = async (req: Request, res: Response, next: Ne
|
||||
}
|
||||
};
|
||||
|
||||
export const withCanRecordPermission = async (req: Request, res: Response, next: NextFunction) => {
|
||||
const roomId = extractRoomIdFromRequest(req);
|
||||
|
||||
const requestSessionService = container.get(RequestSessionService);
|
||||
const tokenRoomId = requestSessionService.getRoomIdFromToken();
|
||||
const permissions = requestSessionService.getRoomMemberMeetPermissions();
|
||||
|
||||
if (!tokenRoomId || !permissions) {
|
||||
const error = errorInsufficientPermissions();
|
||||
return rejectRequestFromMeetError(res, error);
|
||||
}
|
||||
|
||||
if (tokenRoomId !== roomId || !permissions.canRecord) {
|
||||
const error = errorInsufficientPermissions();
|
||||
return rejectRequestFromMeetError(res, error);
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
|
||||
export const withCanRetrieveRecordingsPermission = async (req: Request, res: Response, next: NextFunction) => {
|
||||
const roomId = extractRoomIdFromRequest(req);
|
||||
|
||||
const requestSessionService = container.get(RequestSessionService);
|
||||
const tokenRoomId = requestSessionService.getRoomIdFromToken();
|
||||
|
||||
/**
|
||||
* 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 is using the public access secret.
|
||||
* - The user is using the private access secret and is authenticated.
|
||||
*/
|
||||
if (!tokenRoomId) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const permissions = requestSessionService.getRoomMemberMeetPermissions();
|
||||
|
||||
if (!permissions) {
|
||||
const error = errorInsufficientPermissions();
|
||||
return rejectRequestFromMeetError(res, error);
|
||||
}
|
||||
|
||||
const sameRoom = roomId ? tokenRoomId === roomId : true;
|
||||
|
||||
if (!sameRoom || !permissions.canRetrieveRecordings) {
|
||||
const error = errorInsufficientPermissions();
|
||||
return rejectRequestFromMeetError(res, error);
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
|
||||
export const withCanDeleteRecordingsPermission = async (req: Request, res: Response, next: NextFunction) => {
|
||||
const roomId = extractRoomIdFromRequest(req);
|
||||
|
||||
const requestSessionService = container.get(RequestSessionService);
|
||||
const tokenRoomId = requestSessionService.getRoomIdFromToken();
|
||||
|
||||
// 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 (!tokenRoomId) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const permissions = requestSessionService.getRoomMemberMeetPermissions();
|
||||
|
||||
if (!permissions) {
|
||||
const error = errorInsufficientPermissions();
|
||||
return rejectRequestFromMeetError(res, error);
|
||||
}
|
||||
|
||||
const sameRoom = roomId ? tokenRoomId === roomId : true;
|
||||
|
||||
if (!sameRoom || !permissions.canDeleteRecordings) {
|
||||
const error = errorInsufficientPermissions();
|
||||
return rejectRequestFromMeetError(res, error);
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
|
||||
/**
|
||||
* Middleware to configure authentication for retrieving recording based on the provided secret.
|
||||
*
|
||||
* - If a valid secret is provided in the query, access is granted according to the secret type.
|
||||
* - If no secret is provided, the default authentication logic is applied, i.e., API key, admin and room member token access.
|
||||
*/
|
||||
export const configureRecordingAuth = async (req: Request, res: Response, next: NextFunction) => {
|
||||
export const setupRecordingAuthentication = async (req: Request, res: Response, next: NextFunction) => {
|
||||
const secret = req.query.secret as string;
|
||||
|
||||
// If a secret is provided, validate it against the stored secrets
|
||||
@ -151,8 +67,10 @@ export const configureRecordingAuth = async (req: Request, res: Response, next:
|
||||
authValidators.push(allowAnonymous);
|
||||
break;
|
||||
case recordingSecrets.privateAccessSecret:
|
||||
// Private access secret requires authentication with user role
|
||||
authValidators.push(tokenAndRoleValidator(MeetUserRole.USER));
|
||||
// Private access secret requires authentication
|
||||
authValidators.push(
|
||||
tokenAndRoleValidator(MeetUserRole.ADMIN, MeetUserRole.USER, MeetUserRole.ROOM_MEMBER)
|
||||
);
|
||||
break;
|
||||
default:
|
||||
// Invalid secret provided
|
||||
@ -166,23 +84,96 @@ export const configureRecordingAuth = async (req: Request, res: Response, next:
|
||||
}
|
||||
|
||||
// If no secret is provided, we proceed with the default authentication logic.
|
||||
// This will allow API key, admin and room member token access.
|
||||
const authValidators = [apiKeyValidator, tokenAndRoleValidator(MeetUserRole.ADMIN), roomMemberTokenValidator];
|
||||
// This will allow API key, registered user and room member token access.
|
||||
const authValidators = [
|
||||
apiKeyValidator,
|
||||
tokenAndRoleValidator(MeetUserRole.ADMIN, MeetUserRole.USER, MeetUserRole.ROOM_MEMBER),
|
||||
roomMemberTokenValidator
|
||||
];
|
||||
return withAuth(...authValidators)(req, res, next);
|
||||
};
|
||||
|
||||
const extractRoomIdFromRequest = (req: Request): string | undefined => {
|
||||
if (req.body.roomId) {
|
||||
return req.body.roomId as string;
|
||||
}
|
||||
/**
|
||||
* Middleware to authorize recording access (retrieval or deletion).
|
||||
*
|
||||
* - If a secret is provided in the request query, and allowSecretAccess is true,
|
||||
* it assumes the secret has been validated and grants access.
|
||||
* - If a Room Member Token is used, it checks that the token's roomId matches the requested roomId
|
||||
* and that the member has the required permission.
|
||||
* - If a registered user is authenticated, it checks their role and whether they are the owner or a member of the room
|
||||
* with the required permission.
|
||||
* - If neither a valid token nor an authenticated user is present, it rejects the request.
|
||||
*
|
||||
* @param permission - The permission to check (canRetrieveRecordings or canDeleteRecordings).
|
||||
* @param allowSecretAccess - Whether to allow access based on a valid secret in the query.
|
||||
*/
|
||||
export const authorizeRecordingAccess = (permission: keyof MeetRoomMemberPermissions, allowSecretAccess = false) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
const roomId = MeetRoomHelper.getRoomIdFromRequest(req);
|
||||
const secret = req.query.secret as string;
|
||||
|
||||
// If roomId is not in the body, check if it's in the params
|
||||
const recordingId = req.params.recordingId as string;
|
||||
// If allowSecretAccess is true and a secret is provided,
|
||||
// we assume it has been validated by setupRecordingAuthentication.
|
||||
if (allowSecretAccess && secret) {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (!recordingId) {
|
||||
return undefined;
|
||||
}
|
||||
const requestSessionService = container.get(RequestSessionService);
|
||||
const roomService = container.get(RoomService);
|
||||
|
||||
const { roomId } = RecordingHelper.extractInfoFromRecordingId(recordingId);
|
||||
return roomId;
|
||||
const memberRoomId = requestSessionService.getRoomIdFromMember();
|
||||
const user = requestSessionService.getAuthenticatedUser();
|
||||
|
||||
const forbiddenError = errorInsufficientPermissions();
|
||||
|
||||
// Case 1: Room Member Token
|
||||
if (memberRoomId) {
|
||||
const permissions = requestSessionService.getRoomMemberPermissions();
|
||||
|
||||
if (!permissions) {
|
||||
return rejectRequestFromMeetError(res, forbiddenError);
|
||||
}
|
||||
|
||||
const sameRoom = roomId ? memberRoomId === roomId : true;
|
||||
|
||||
if (!sameRoom || !permissions[permission]) {
|
||||
return rejectRequestFromMeetError(res, forbiddenError);
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
||||
|
||||
// Case 2: Authenticated User
|
||||
if (user) {
|
||||
// If no roomId is specified, we are in a listing/bulk request
|
||||
// Each recording's room ownership and permissions will be checked individually
|
||||
if (!roomId) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Admins can always access
|
||||
if (user.role === MeetUserRole.ADMIN) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Check if owner
|
||||
const isOwner = await roomService.isRoomOwner(roomId, user.userId);
|
||||
|
||||
if (isOwner) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Check if member with permissions
|
||||
const member = await roomService.getRoomMember(roomId, user.userId);
|
||||
|
||||
if (member && member.effectivePermissions[permission]) {
|
||||
return next();
|
||||
}
|
||||
|
||||
return rejectRequestFromMeetError(res, forbiddenError);
|
||||
}
|
||||
|
||||
// Otherwise, reject the request
|
||||
return rejectRequestFromMeetError(res, forbiddenError);
|
||||
};
|
||||
};
|
||||
|
||||
103
meet-ce/backend/src/middlewares/room-member.middleware.ts
Normal file
103
meet-ce/backend/src/middlewares/room-member.middleware.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import { MeetRoomMemberPermissions, MeetRoomMemberTokenOptions, MeetUserRole } from '@openvidu-meet/typings';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import { container } from '../config/dependency-injector.config.js';
|
||||
import { MeetRoomHelper } from '../helpers/room.helper.js';
|
||||
import { errorInsufficientPermissions, rejectRequestFromMeetError } from '../models/error.model.js';
|
||||
import { RequestSessionService } from '../services/request-session.service.js';
|
||||
import { RoomService } from '../services/room.service.js';
|
||||
import { allowAnonymous, AuthValidator, tokenAndRoleValidator, withAuth } from './auth.middleware.js';
|
||||
|
||||
/**
|
||||
* Middleware to configure authentication for generating room member tokens.
|
||||
*
|
||||
* - If a secret is provided in the request body, anonymous access is allowed.
|
||||
* - If no secret is provided, the user must be authenticated as ADMIN, USER, or ROOM_MEMBER.
|
||||
*/
|
||||
export const setupRoomMemberTokenAuthentication = async (req: Request, res: Response, next: NextFunction) => {
|
||||
const { secret } = req.body as MeetRoomMemberTokenOptions;
|
||||
const authValidators: AuthValidator[] = [];
|
||||
|
||||
if (secret) {
|
||||
authValidators.push(allowAnonymous);
|
||||
} else {
|
||||
authValidators.push(tokenAndRoleValidator(MeetUserRole.ADMIN, MeetUserRole.USER, MeetUserRole.ROOM_MEMBER));
|
||||
}
|
||||
|
||||
return withAuth(...authValidators)(req, res, next);
|
||||
};
|
||||
|
||||
/**
|
||||
* Middleware to authorize the generation of a room member token.
|
||||
*
|
||||
* - If a secret is provided, it checks if it matches a valid room secret (anonymous access) or if it corresponds to a room member.
|
||||
* - If no secret is provided, it checks if the authenticated user has permissions to access the room (Admin, Owner, or Member).
|
||||
*/
|
||||
export const authorizeRoomMemberTokenGeneration = async (req: Request, res: Response, next: NextFunction) => {
|
||||
const { roomId } = req.params;
|
||||
const { secret } = req.body as MeetRoomMemberTokenOptions;
|
||||
|
||||
const requestSessionService = container.get(RequestSessionService);
|
||||
const roomService = container.get(RoomService);
|
||||
const user = requestSessionService.getAuthenticatedUser();
|
||||
|
||||
const forbiddenError = errorInsufficientPermissions();
|
||||
|
||||
// Scenario 1: Secret provided (Anonymous access or Member ID)
|
||||
if (secret) {
|
||||
// Check if secret matches any room access URL secret
|
||||
const isValidSecret = await roomService.isValidRoomSecret(roomId, secret);
|
||||
|
||||
if (isValidSecret) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Check if secret is a memberId
|
||||
const isMember = await roomService.isRoomMember(roomId, secret);
|
||||
|
||||
if (isMember) {
|
||||
return next();
|
||||
}
|
||||
|
||||
return rejectRequestFromMeetError(res, forbiddenError);
|
||||
}
|
||||
|
||||
// Scenario 2: No secret provided (Authenticated User)
|
||||
if (user) {
|
||||
const canAccess = await roomService.canUserAccessRoom(roomId, user);
|
||||
|
||||
if (!canAccess) {
|
||||
return rejectRequestFromMeetError(res, forbiddenError);
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
||||
|
||||
return rejectRequestFromMeetError(res, forbiddenError);
|
||||
};
|
||||
|
||||
/**
|
||||
* Middleware to check if the room member has a specific permission.
|
||||
*
|
||||
* @param permission The permission to check (key of MeetRoomMemberPermissions).
|
||||
*/
|
||||
export const withRoomMemberPermission = (permission: keyof MeetRoomMemberPermissions) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
const roomId = MeetRoomHelper.getRoomIdFromRequest(req);
|
||||
|
||||
const requestSessionService = container.get(RequestSessionService);
|
||||
const memberRoomId = requestSessionService.getRoomIdFromMember();
|
||||
const permissions = requestSessionService.getRoomMemberPermissions();
|
||||
|
||||
if (!memberRoomId || !permissions) {
|
||||
const error = errorInsufficientPermissions();
|
||||
return rejectRequestFromMeetError(res, error);
|
||||
}
|
||||
|
||||
if (memberRoomId !== roomId || !permissions[permission]) {
|
||||
const error = errorInsufficientPermissions();
|
||||
return rejectRequestFromMeetError(res, error);
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
};
|
||||
@ -2,31 +2,46 @@ import { NextFunction, Request, Response } from 'express';
|
||||
import { container } from '../config/dependency-injector.config.js';
|
||||
import { errorInsufficientPermissions, rejectRequestFromMeetError } from '../models/error.model.js';
|
||||
import { RequestSessionService } from '../services/request-session.service.js';
|
||||
import { RoomService } from '../services/room.service.js';
|
||||
|
||||
/**
|
||||
* Middleware that configures authorization for accessing a specific room.
|
||||
* Middleware to authorize access to a room.
|
||||
*
|
||||
* - If there is no token in the session, the user is granted access (admin or API key).
|
||||
* - If the user does not belong to the requested room, access is denied.
|
||||
* - Otherwise, the user is allowed to access the room.
|
||||
* - If a Room Member Token is used, it checks that the token's roomId matches the requested roomId.
|
||||
* - If a registered user is authenticated, it checks their role and whether they are the owner or a member of the room.
|
||||
* - If neither a valid token nor an authenticated user is present, it rejects the request.
|
||||
*/
|
||||
export const configureRoomAuthorization = async (req: Request, res: Response, next: NextFunction) => {
|
||||
export const authorizeRoomAccess = async (req: Request, res: Response, next: NextFunction) => {
|
||||
const roomId = req.params.roomId as string;
|
||||
|
||||
const requestSessionService = container.get(RequestSessionService);
|
||||
const tokenRoomId = requestSessionService.getRoomIdFromToken();
|
||||
const memberRoomId = requestSessionService.getRoomIdFromMember();
|
||||
const user = requestSessionService.getAuthenticatedUser();
|
||||
|
||||
const forbiddenError = errorInsufficientPermissions();
|
||||
|
||||
// Room Member Token
|
||||
if (memberRoomId) {
|
||||
// Check if the member's roomId matches the requested roomId
|
||||
if (memberRoomId !== roomId) {
|
||||
return rejectRequestFromMeetError(res, forbiddenError);
|
||||
}
|
||||
|
||||
// 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 (!tokenRoomId) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// If the user does not belong to the requested room, access is denied
|
||||
if (tokenRoomId !== roomId) {
|
||||
const error = errorInsufficientPermissions();
|
||||
return rejectRequestFromMeetError(res, error);
|
||||
// Registered User
|
||||
if (user) {
|
||||
const roomService = container.get(RoomService);
|
||||
const canAccess = await roomService.canUserAccessRoom(roomId, user);
|
||||
|
||||
if (!canAccess) {
|
||||
return rejectRequestFromMeetError(res, forbiddenError);
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
||||
|
||||
return next();
|
||||
// If there is no token and no user, reject the request
|
||||
return rejectRequestFromMeetError(res, forbiddenError);
|
||||
};
|
||||
|
||||
@ -2,7 +2,7 @@ import bodyParser from 'body-parser';
|
||||
import { Router } from 'express';
|
||||
import * as meetingCtrl from '../controllers/meeting.controller.js';
|
||||
import { roomMemberTokenValidator, withAuth } from '../middlewares/auth.middleware.js';
|
||||
import { withModeratorPermissions } from '../middlewares/room-member.middleware.js';
|
||||
import { withRoomMemberPermission } from '../middlewares/room-member.middleware.js';
|
||||
import { validateUpdateParticipantRoleReq } from '../middlewares/request-validators/meeting-validator.middleware.js';
|
||||
import { withValidRoomId } from '../middlewares/request-validators/room-validator.middleware.js';
|
||||
|
||||
@ -15,21 +15,21 @@ internalMeetingRouter.delete(
|
||||
'/:roomId',
|
||||
withAuth(roomMemberTokenValidator),
|
||||
withValidRoomId,
|
||||
withModeratorPermissions,
|
||||
withRoomMemberPermission('canEndMeeting'),
|
||||
meetingCtrl.endMeeting
|
||||
);
|
||||
internalMeetingRouter.delete(
|
||||
'/:roomId/participants/:participantIdentity',
|
||||
withAuth(roomMemberTokenValidator),
|
||||
withValidRoomId,
|
||||
withModeratorPermissions,
|
||||
withRoomMemberPermission('canKickParticipants'),
|
||||
meetingCtrl.kickParticipantFromMeeting
|
||||
);
|
||||
internalMeetingRouter.put(
|
||||
'/:roomId/participants/:participantIdentity/role',
|
||||
withAuth(roomMemberTokenValidator),
|
||||
withValidRoomId,
|
||||
withModeratorPermissions,
|
||||
withRoomMemberPermission('canMakeModerator'),
|
||||
validateUpdateParticipantRoleReq,
|
||||
meetingCtrl.updateParticipantRole
|
||||
);
|
||||
|
||||
@ -9,10 +9,8 @@ import {
|
||||
withAuth
|
||||
} from '../middlewares/auth.middleware.js';
|
||||
import {
|
||||
configureRecordingAuth,
|
||||
withCanDeleteRecordingsPermission,
|
||||
withCanRecordPermission,
|
||||
withCanRetrieveRecordingsPermission,
|
||||
authorizeRecordingAccess,
|
||||
setupRecordingAuthentication,
|
||||
withRecordingEnabled
|
||||
} from '../middlewares/recording.middleware.js';
|
||||
import {
|
||||
@ -24,6 +22,7 @@ import {
|
||||
validateStartRecordingReq,
|
||||
withValidRecordingId
|
||||
} from '../middlewares/request-validators/recording-validator.middleware.js';
|
||||
import { withRoomMemberPermission } from '../middlewares/room-member.middleware.js';
|
||||
|
||||
export const recordingRouter: Router = Router();
|
||||
recordingRouter.use(bodyParser.urlencoded({ extended: true }));
|
||||
@ -37,8 +36,8 @@ recordingRouter.get(
|
||||
tokenAndRoleValidator(MeetUserRole.ADMIN, MeetUserRole.USER, MeetUserRole.ROOM_MEMBER),
|
||||
roomMemberTokenValidator
|
||||
),
|
||||
withCanRetrieveRecordingsPermission,
|
||||
validateGetRecordingsReq,
|
||||
authorizeRecordingAccess('canRetrieveRecordings'),
|
||||
recordingCtrl.getRecordings
|
||||
);
|
||||
recordingRouter.delete(
|
||||
@ -49,7 +48,7 @@ recordingRouter.delete(
|
||||
roomMemberTokenValidator
|
||||
),
|
||||
validateBulkDeleteRecordingsReq,
|
||||
withCanDeleteRecordingsPermission,
|
||||
authorizeRecordingAccess('canDeleteRecordings'),
|
||||
recordingCtrl.bulkDeleteRecordings
|
||||
);
|
||||
recordingRouter.get(
|
||||
@ -60,14 +59,14 @@ recordingRouter.get(
|
||||
roomMemberTokenValidator
|
||||
),
|
||||
validateBulkDeleteRecordingsReq,
|
||||
withCanRetrieveRecordingsPermission,
|
||||
authorizeRecordingAccess('canRetrieveRecordings'),
|
||||
recordingCtrl.downloadRecordingsZip
|
||||
);
|
||||
recordingRouter.get(
|
||||
'/:recordingId',
|
||||
validateGetRecordingReq,
|
||||
configureRecordingAuth,
|
||||
withCanRetrieveRecordingsPermission,
|
||||
setupRecordingAuthentication,
|
||||
authorizeRecordingAccess('canRetrieveRecordings', true),
|
||||
recordingCtrl.getRecording
|
||||
);
|
||||
recordingRouter.delete(
|
||||
@ -78,14 +77,14 @@ recordingRouter.delete(
|
||||
roomMemberTokenValidator
|
||||
),
|
||||
withValidRecordingId,
|
||||
withCanDeleteRecordingsPermission,
|
||||
authorizeRecordingAccess('canDeleteRecordings'),
|
||||
recordingCtrl.deleteRecording
|
||||
);
|
||||
recordingRouter.get(
|
||||
'/:recordingId/media',
|
||||
validateGetRecordingMediaReq,
|
||||
configureRecordingAuth,
|
||||
withCanRetrieveRecordingsPermission,
|
||||
setupRecordingAuthentication,
|
||||
authorizeRecordingAccess('canRetrieveRecordings', true),
|
||||
recordingCtrl.getRecordingMedia
|
||||
);
|
||||
recordingRouter.get(
|
||||
@ -96,7 +95,7 @@ recordingRouter.get(
|
||||
roomMemberTokenValidator
|
||||
),
|
||||
validateGetRecordingUrlReq,
|
||||
withCanRetrieveRecordingsPermission,
|
||||
authorizeRecordingAccess('canRetrieveRecordings'),
|
||||
recordingCtrl.getRecordingUrl
|
||||
);
|
||||
|
||||
@ -110,7 +109,7 @@ internalRecordingRouter.post(
|
||||
validateStartRecordingReq,
|
||||
withRecordingEnabled,
|
||||
withAuth(roomMemberTokenValidator),
|
||||
withCanRecordPermission,
|
||||
withRoomMemberPermission('canRecord'),
|
||||
recordingCtrl.startRecording
|
||||
);
|
||||
internalRecordingRouter.post(
|
||||
@ -118,6 +117,6 @@ internalRecordingRouter.post(
|
||||
withValidRecordingId,
|
||||
withRecordingEnabled,
|
||||
withAuth(roomMemberTokenValidator),
|
||||
withCanRecordPermission,
|
||||
withRoomMemberPermission('canRecord'),
|
||||
recordingCtrl.stopRecording
|
||||
);
|
||||
|
||||
@ -9,7 +9,6 @@ import {
|
||||
tokenAndRoleValidator,
|
||||
withAuth
|
||||
} from '../middlewares/auth.middleware.js';
|
||||
import { configureRoomMemberTokenAuth } from '../middlewares/room-member.middleware.js';
|
||||
import {
|
||||
validateBulkDeleteRoomMembersReq,
|
||||
validateCreateRoomMemberReq,
|
||||
@ -28,7 +27,11 @@ import {
|
||||
validateUpdateRoomStatusReq,
|
||||
withValidRoomId
|
||||
} from '../middlewares/request-validators/room-validator.middleware.js';
|
||||
import { configureRoomAuthorization } from '../middlewares/room.middleware.js';
|
||||
import {
|
||||
authorizeRoomMemberTokenGeneration,
|
||||
setupRoomMemberTokenAuthentication
|
||||
} from '../middlewares/room-member.middleware.js';
|
||||
import { authorizeRoomAccess } from '../middlewares/room.middleware.js';
|
||||
|
||||
export const roomRouter: Router = Router();
|
||||
roomRouter.use(bodyParser.urlencoded({ extended: true }));
|
||||
@ -62,7 +65,7 @@ roomRouter.get(
|
||||
roomMemberTokenValidator
|
||||
),
|
||||
withValidRoomId,
|
||||
configureRoomAuthorization,
|
||||
authorizeRoomAccess,
|
||||
roomCtrl.getRoom
|
||||
);
|
||||
roomRouter.delete(
|
||||
@ -80,7 +83,7 @@ roomRouter.get(
|
||||
roomMemberTokenValidator
|
||||
),
|
||||
withValidRoomId,
|
||||
configureRoomAuthorization,
|
||||
authorizeRoomAccess,
|
||||
roomCtrl.getRoomConfig
|
||||
);
|
||||
roomRouter.put(
|
||||
@ -123,7 +126,7 @@ roomRouter.post(
|
||||
);
|
||||
roomRouter.get(
|
||||
'/:roomId/members',
|
||||
withAuth(apiKeyValidator, tokenAndRoleValidator(MeetUserRole.ADMIN, MeetUserRole.USER), roomMemberTokenValidator),
|
||||
withAuth(apiKeyValidator, tokenAndRoleValidator(MeetUserRole.ADMIN, MeetUserRole.USER, MeetUserRole.ROOM_MEMBER)),
|
||||
withValidRoomId,
|
||||
validateGetRoomMembersReq,
|
||||
roomMemberCtrl.getRoomMembers
|
||||
@ -169,6 +172,7 @@ internalRoomRouter.post(
|
||||
'/:roomId/members/token',
|
||||
withValidRoomId,
|
||||
validateCreateRoomMemberTokenReq,
|
||||
configureRoomMemberTokenAuth,
|
||||
setupRoomMemberTokenAuthentication,
|
||||
authorizeRoomMemberTokenGeneration,
|
||||
roomMemberCtrl.generateRoomMemberToken
|
||||
);
|
||||
|
||||
@ -2,7 +2,8 @@ import {
|
||||
MeetRoomMemberPermissions,
|
||||
MeetRoomMemberRole,
|
||||
MeetRoomMemberTokenMetadata,
|
||||
MeetUser
|
||||
MeetUser,
|
||||
MeetUserRole
|
||||
} from '@openvidu-meet/typings';
|
||||
import { AsyncLocalStorage } from 'async_hooks';
|
||||
import { injectable } from 'inversify';
|
||||
@ -75,6 +76,13 @@ export class RequestSessionService {
|
||||
return this.getContext()?.user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the authenticated user's role from the current request context.
|
||||
*/
|
||||
getAuthenticatedUserRole(): MeetUserRole | undefined {
|
||||
return this.getContext()?.user?.role;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the room member token metadata (room ID, base role, permissions)
|
||||
* in the current request context.
|
||||
|
||||
@ -8,9 +8,12 @@ import {
|
||||
MeetRoomDeletionPolicyWithRecordings,
|
||||
MeetRoomDeletionSuccessCode,
|
||||
MeetRoomFilters,
|
||||
MeetRoomMember,
|
||||
MeetRoomMemberRole,
|
||||
MeetRoomOptions,
|
||||
MeetRoomStatus
|
||||
MeetRoomStatus,
|
||||
MeetUser,
|
||||
MeetUserRole
|
||||
} from '@openvidu-meet/typings';
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { CreateOptions, Room } from 'livekit-server-sdk';
|
||||
@ -233,7 +236,7 @@ export class RoomService {
|
||||
}
|
||||
|
||||
// Remove moderatorUrl if the room member is a speaker to prevent access to moderator links
|
||||
const role = this.requestSessionService.getRoomMemberRole();
|
||||
const role = this.requestSessionService.getRoomMemberBaseRole();
|
||||
|
||||
if (role === MeetRoomMemberRole.SPEAKER) {
|
||||
delete (room as Partial<MeetRoom>).moderatorUrl;
|
||||
@ -634,4 +637,70 @@ export class RoomService {
|
||||
);
|
||||
return { successful, failed };
|
||||
}
|
||||
|
||||
async isRoomOwner(roomId: string, userId: string): Promise<boolean> {
|
||||
// TODO: Implement
|
||||
return false;
|
||||
}
|
||||
|
||||
async isRoomMember(roomId: string, memberId: string): Promise<boolean> {
|
||||
// TODO: Implement
|
||||
return false;
|
||||
}
|
||||
|
||||
async getRoomMember(roomId: string, userId: string): Promise<MeetRoomMember | null> {
|
||||
// TODO: Implement
|
||||
return null;
|
||||
}
|
||||
|
||||
async isValidRoomSecret(roomId: string, secret: string): Promise<boolean> {
|
||||
// TODO: Implement
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a registered user can access a specific room based on their role.
|
||||
*
|
||||
* @param roomId The ID of the room to check access for.
|
||||
* @param user The user object containing user details and role.
|
||||
* @returns A promise that resolves to true if the user can access the room, false otherwise.
|
||||
*/
|
||||
async canUserAccessRoom(roomId: string, user: MeetUser): Promise<boolean> {
|
||||
switch (user.role) {
|
||||
case MeetUserRole.ADMIN:
|
||||
// Admins can access all rooms
|
||||
return true;
|
||||
|
||||
case MeetUserRole.USER: {
|
||||
// Users can access rooms they own or are members of
|
||||
const isOwner = await this.isRoomOwner(roomId, user.userId);
|
||||
|
||||
if (isOwner) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const isMember = await this.isRoomMember(roomId, user.userId);
|
||||
|
||||
if (isMember) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
case MeetUserRole.ROOM_MEMBER: {
|
||||
// Room members can only access rooms they are members of
|
||||
const isMember = await this.isRoomMember(roomId, user.userId);
|
||||
|
||||
if (isMember) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user