mirror of
https://github.com/suitenumerique/meet.git
synced 2026-10-12 16:07:49 +00:00
b3705af768
Any authenticated participant can now start a recording on trusted and public rooms. This is essential, requested by many users, and should encourage people to authenticate on public rooms. It also means the starter may hold no role on the room, so who gets access to the recording has to be decided explicitly. The part open to discussion is the recording access: who should be owner/admin on the recording. This commit picks the simplest design: * The starter and the room's owners and administrators all get an `OWNER` `RecordingAccess`, so they can retrieve, download and delete the recording, and they all receive the "recording ready" email. * Room roles are copied when the recording starts: later changes to the room's accesses do not affect existing recordings. The people responsible for a meeting should get access to its recording and transcript, along with whoever initiated it. One constraint is respected: the summary service creates a doc, and the docs API can only attribute it to a single owner. The summary therefore goes to the oldest `OWNER`, and the starter's access is created before the room owners/admins ones, so the starter receives it. The previous lookup took `.first()` with the default newest-first ordering, which with several owners would have picked a room admin. Alternatives considered: * A `creator` attribute on the recording row: it only adds a migration without adding value to access control, so it was dropped. * Restricting access to the link owner even when someone else starts the recording: some organizations may want this for security reasons, but it is YAGNI until a few large clients ask for it. I think we should contribute to Docs' api to allow sharing the transcription document with several users.
252 lines
8.1 KiB
Python
252 lines
8.1 KiB
Python
"""Permission handlers for the Meet core app."""
|
|
|
|
from django.conf import settings
|
|
from django.http import Http404
|
|
|
|
from rest_framework import permissions
|
|
|
|
from ..models import RoleChoices, RoomAccessLevel
|
|
from ..services.participants_management import (
|
|
ParticipantNotFoundException,
|
|
ParticipantsManagement,
|
|
ParticipantsManagementException,
|
|
)
|
|
|
|
|
|
class IsAuthenticated(permissions.BasePermission):
|
|
"""
|
|
Allows access only to authenticated users. Alternative method checking the presence
|
|
of the auth token to avoid hitting the database.
|
|
"""
|
|
|
|
def has_permission(self, request, view):
|
|
return bool(request.auth) or request.user.is_authenticated
|
|
|
|
|
|
class IsSelf(IsAuthenticated):
|
|
"""
|
|
Allows access only to authenticated users. Alternative method checking the presence
|
|
of the auth token to avoid hitting the database.
|
|
"""
|
|
|
|
def has_object_permission(self, request, view, obj):
|
|
"""Write permissions are only allowed to the user itself."""
|
|
return obj == request.user
|
|
|
|
|
|
class RoomPermissions(permissions.BasePermission):
|
|
"""
|
|
Permissions applying to the room API endpoint.
|
|
"""
|
|
|
|
def has_permission(self, request, view):
|
|
"""Only allow authenticated users for unsafe methods."""
|
|
if request.method in permissions.SAFE_METHODS:
|
|
return True
|
|
|
|
return request.user.is_authenticated
|
|
|
|
def has_object_permission(self, request, view, obj):
|
|
"""Object permissions are only given to administrators of the room."""
|
|
|
|
if request.method in permissions.SAFE_METHODS:
|
|
return True
|
|
|
|
user = request.user
|
|
|
|
if request.method == "DELETE":
|
|
return obj.is_owner(user)
|
|
|
|
return obj.is_administrator_or_owner(user)
|
|
|
|
|
|
class ResourceAccessPermission(IsAuthenticated):
|
|
"""
|
|
Permissions for a room that can only be updated by room administrators.
|
|
"""
|
|
|
|
def has_object_permission(self, request, view, obj):
|
|
"""
|
|
Check that the logged-in user is administrator of the linked room.
|
|
"""
|
|
user = request.user
|
|
if request.method == "DELETE" and obj.role == RoleChoices.OWNER:
|
|
return obj.user == user
|
|
|
|
return obj.resource.is_administrator_or_owner(user)
|
|
|
|
|
|
class HasAbilityPermission(IsAuthenticated):
|
|
"""Permission class for access objects."""
|
|
|
|
def has_object_permission(self, request, view, obj):
|
|
"""Check permission for a given object."""
|
|
return obj.get_abilities(request.user).get(view.action, False)
|
|
|
|
|
|
class HasPrivilegesOnRoom(IsAuthenticated):
|
|
"""Check if user has privileges on a given room."""
|
|
|
|
message = "You must have privileges on room to perform this action."
|
|
|
|
def has_object_permission(self, request, view, obj):
|
|
"""Determine if user has privileges on room."""
|
|
return obj.is_administrator_or_owner(request.user)
|
|
|
|
|
|
class HasLiveKitRoomAccess(permissions.BasePermission):
|
|
"""Check if authenticated user's LiveKit token is for the specific room."""
|
|
|
|
def has_object_permission(self, request, view, obj):
|
|
if not request.auth or not hasattr(request.auth, "video"):
|
|
return False
|
|
return request.auth.video.room == str(obj.id)
|
|
|
|
|
|
class FilePermission(IsAuthenticated):
|
|
"""
|
|
Permissions applying to the file API endpoint.
|
|
Handling soft deletions specificities
|
|
"""
|
|
|
|
def has_permission(self, request, view):
|
|
"""Allow access only to authenticated users."""
|
|
if not settings.FILE_UPLOAD_ENABLED:
|
|
raise Http404
|
|
|
|
return super().has_permission(request, view)
|
|
|
|
def has_object_permission(self, request, view, obj):
|
|
"""
|
|
Return a 404 on deleted files or if the user is not the owner
|
|
"""
|
|
|
|
if obj.deleted_at is not None or obj.hard_deleted_at is not None:
|
|
raise Http404
|
|
|
|
if obj.creator != request.user:
|
|
raise Http404
|
|
|
|
return obj.get_abilities(request.user).get(view.action, False)
|
|
|
|
|
|
class CanMuteParticipant(permissions.BasePermission):
|
|
"""
|
|
Grant muting rights based on role or room configuration.
|
|
|
|
- Admins and owners can always mute.
|
|
- When `everyone_can_mute` is enabled on the room, any participant
|
|
currently in the room (proven by a valid LiveKit token for that room)
|
|
can mute.
|
|
"""
|
|
|
|
def has_object_permission(self, request, view, obj):
|
|
"""Check if the requesting user is allowed to mute a participant in the given room."""
|
|
|
|
is_livekit_token_auth = request.auth and hasattr(request.auth, "video")
|
|
|
|
# Always allow admins/owners when authenticated with session cookie
|
|
if not is_livekit_token_auth and obj.is_administrator_or_owner(request.user):
|
|
return True
|
|
|
|
everyone_can_mute = obj.configuration.get("everyone_can_mute", True)
|
|
if not everyone_can_mute:
|
|
return False
|
|
|
|
if not is_livekit_token_auth:
|
|
return False
|
|
|
|
# LiveKit token scoped to this room
|
|
return request.auth.video.room == str(obj.id)
|
|
|
|
|
|
class IsPresentInMeeting(permissions.BasePermission):
|
|
"""Check that the requesting user is currently connected to the meeting.
|
|
|
|
The requester must be session-authenticated (their DB identity is needed
|
|
to check privileges); presence is verified against LiveKit using their
|
|
`sub` as participant identity. Fails closed on LiveKit errors.
|
|
"""
|
|
|
|
message = "You must be connected to the meeting to perform this action."
|
|
|
|
def has_object_permission(self, request, view, obj):
|
|
"""Verify the requester's identity is a participant of the room."""
|
|
user = request.user
|
|
|
|
if not user or not user.is_authenticated:
|
|
return False
|
|
|
|
try:
|
|
return ParticipantsManagement().check_if_in_meeting(
|
|
room_name=str(obj.pk), identity=str(user.sub)
|
|
)
|
|
except ParticipantNotFoundException:
|
|
return False
|
|
except ParticipantsManagementException:
|
|
return False
|
|
|
|
|
|
class ParticipantCapability(permissions.BasePermission):
|
|
"""Base permission for in-meeting capabilities opened up to participants.
|
|
|
|
- Room admins/owners always hold the capability.
|
|
- When the room access level is TRUSTED or PUBLIC, any
|
|
session-authenticated user who is currently connected to the meeting
|
|
holds it too.
|
|
|
|
Access level is always read fresh from the DB; only presence is cached,
|
|
so changing the room access level takes effect immediately.
|
|
"""
|
|
|
|
message = "You are not allowed to perform this action."
|
|
enabled_setting = None
|
|
|
|
def has_object_permission(self, request, view, obj):
|
|
"""Check privileges first, then the participant presence path."""
|
|
user = request.user
|
|
|
|
if not user or not user.is_authenticated:
|
|
return False
|
|
|
|
# Product choice: these capabilities are reserved for session-authenticated
|
|
# users with a real account, not holders of a LiveKit room token.
|
|
if request.auth and hasattr(request.auth, "video"):
|
|
return False
|
|
|
|
if obj.is_administrator_or_owner(user):
|
|
return True
|
|
|
|
is_open_room = obj.access_level in (
|
|
RoomAccessLevel.TRUSTED,
|
|
RoomAccessLevel.PUBLIC,
|
|
)
|
|
is_enabled = not self.enabled_setting or getattr(settings, self.enabled_setting)
|
|
if not (is_open_room and is_enabled):
|
|
return False
|
|
|
|
try:
|
|
return ParticipantsManagement().check_if_in_meeting_cached(
|
|
room_name=str(obj.pk), identity=str(user.sub)
|
|
)
|
|
except (ParticipantNotFoundException, ParticipantsManagementException):
|
|
return False
|
|
|
|
|
|
class CanManageLobby(ParticipantCapability):
|
|
"""Grant lobby management (list/accept/deny waiting participants).
|
|
|
|
Public rooms have no lobby: the endpoints short-circuit there, so opening
|
|
them to participants exposes nothing.
|
|
"""
|
|
|
|
|
|
class CanManageRecording(ParticipantCapability):
|
|
"""Grant starting/stopping a room recording (screen recording or transcript).
|
|
|
|
Self-hosters can keep it admin/owner only with
|
|
`RECORDING_AUTHENTICATED_PARTICIPANTS_ENABLED`.
|
|
"""
|
|
|
|
enabled_setting = "RECORDING_AUTHENTICATED_PARTICIPANTS_ENABLED"
|