Files
meet/src/backend/core/api/permissions.py
T
lebaudantoine b3705af768 ✨(all) allow any authenticated user to start a recording
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.
2026-10-10 00:58:56 +02:00

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"