This commit is contained in:
Thomas Ramé
2026-03-30 17:37:05 +02:00
parent 15133f9d6b
commit 58bc6398eb
49 changed files with 2672 additions and 80 deletions
+6
View File
@@ -73,5 +73,11 @@ def get_frontend_configuration(request):
"default_sources": settings.LIVEKIT_DEFAULT_SOURCES,
},
}
if settings.ENCRYPTION_ENABLED and settings.ENCRYPTION_VAULT_URL:
frontend_configuration["encryption"] = {
"enabled": True,
"vault_url": settings.ENCRYPTION_VAULT_URL,
"interface_url": settings.ENCRYPTION_INTERFACE_URL,
}
frontend_configuration.update(settings.FRONTEND_CONFIGURATION)
return Response(frontend_configuration)
+25 -1
View File
@@ -128,9 +128,27 @@ class RoomSerializer(serializers.ModelSerializer):
class Meta:
model = models.Room
fields = ["id", "name", "slug", "configuration", "access_level", "pin_code"]
fields = ["id", "name", "slug", "configuration", "access_level", "pin_code", "encryption_enabled"]
read_only_fields = ["id", "slug", "pin_code"]
def validate_access_level(self, value):
"""Encrypted rooms must stay restricted — prevent downgrading access level."""
instance = self.instance
if instance and instance.encryption_enabled and value != models.RoomAccessLevel.RESTRICTED:
raise serializers.ValidationError(
"Encrypted rooms require restricted access level to enforce lobby approval."
)
return value
def validate_encryption_enabled(self, value):
"""Once encryption is enabled on a room, it cannot be disabled."""
instance = self.instance
if instance and instance.encryption_enabled and not value:
raise serializers.ValidationError(
"Encryption cannot be disabled once enabled on a room."
)
return value
def to_representation(self, instance):
"""
Add users only for administrator users.
@@ -172,6 +190,12 @@ class RoomSerializer(serializers.ModelSerializer):
if should_access_room:
room_id = f"{instance.id!s}"
username = request.query_params.get("username", None)
# In encrypted rooms, authenticated users must use their real name from
# the OIDC profile (ProConnect) — they cannot choose an arbitrary name.
if instance.encryption_enabled and request.user.is_authenticated:
username = request.user.full_name or request.user.email
output["livekit"] = utils.generate_livekit_config(
room_id=room_id,
user=request.user,
+22 -1
View File
@@ -281,6 +281,11 @@ class RoomViewSet(
def perform_create(self, serializer):
"""Set the current user as owner of the newly created room."""
# Encrypted rooms must use restricted access to enforce lobby approval
# before the encryption key is shared with participants.
if serializer.validated_data.get("encryption_enabled"):
serializer.validated_data["access_level"] = models.RoomAccessLevel.RESTRICTED
room = serializer.save()
models.ResourceAccess.objects.create(
resource=room,
@@ -396,12 +401,21 @@ class RoomViewSet(
serializer.is_valid(raise_exception=True)
room = self.get_object()
validated_data = serializer.validated_data
# In encrypted rooms, authenticated users must use their real name
# from the OIDC profile — they cannot choose an arbitrary name.
if room.encryption_enabled and request.user.is_authenticated:
validated_data["username"] = (
request.user.full_name or request.user.email
)
lobby_service = LobbyService()
participant, livekit = lobby_service.request_entry(
room=room,
request=request,
**serializer.validated_data,
**validated_data,
)
response = drf_response.Response({**participant.to_dict(), "livekit": livekit})
lobby_service.prepare_response(response, participant.id)
@@ -464,6 +478,13 @@ class RoomViewSet(
lobby_service = LobbyService()
participants = lobby_service.list_waiting_participants(room.id)
# Only expose email in encrypted rooms (needed for admin identity verification).
# Strip it otherwise to avoid leaking personal data.
if not room.encryption_enabled:
for p in participants:
p.pop("email", None)
return drf_response.Response({"participants": participants})
@decorators.action(
@@ -0,0 +1,20 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0018_rename_active_application_is_active"),
]
operations = [
migrations.AddField(
model_name="room",
name="encryption_enabled",
field=models.BooleanField(
default=False,
help_text="Whether end-to-end encryption is enabled for this room.",
verbose_name="Encryption enabled",
),
),
]
+5
View File
@@ -388,6 +388,11 @@ class Room(Resource):
choices=RoomAccessLevel.choices,
default=settings.RESOURCE_DEFAULT_ACCESS_LEVEL,
)
encryption_enabled = models.BooleanField(
default=False,
verbose_name=_("Encryption enabled"),
help_text=_("Whether end-to-end encryption is enabled for this room."),
)
configuration = models.JSONField(
blank=True,
default=dict,
+19 -3
View File
@@ -46,15 +46,21 @@ class LobbyParticipant:
username: str
color: str
id: str
is_authenticated: bool = False
email: Optional[str] = None
def to_dict(self) -> Dict[str, str]:
"""Serialize the participant object to a dict representation."""
return {
result = {
"status": self.status.value,
"username": self.username,
"id": self.id,
"color": self.color,
"is_authenticated": self.is_authenticated,
}
if self.email:
result["email"] = self.email
return result
@classmethod
def from_dict(cls, data: dict) -> "LobbyParticipant":
@@ -68,6 +74,8 @@ class LobbyParticipant:
username=data["username"],
id=data["id"],
color=data["color"],
is_authenticated=data.get("is_authenticated", False),
email=data.get("email"),
)
except (KeyError, ValueError) as e:
logger.exception("Error creating Participant from dict:")
@@ -170,7 +178,11 @@ class LobbyService:
livekit_config = None
if participant is None:
participant = self.enter(room.id, participant_id, username)
participant = self.enter(
room.id, participant_id, username,
is_authenticated=request.user.is_authenticated,
email=getattr(request.user, 'email', None) if request.user.is_authenticated else None,
)
elif participant.status == LobbyParticipantStatus.WAITING:
self.refresh_waiting_status(room.id, participant_id)
@@ -201,7 +213,9 @@ class LobbyService:
)
def enter(
self, room_id: UUID, participant_id: str, username: str
self, room_id: UUID, participant_id: str, username: str,
is_authenticated: bool = False,
email: Optional[str] = None,
) -> LobbyParticipant:
"""Add participant to waiting lobby.
@@ -216,6 +230,8 @@ class LobbyService:
username=username,
id=participant_id,
color=color,
is_authenticated=is_authenticated,
email=email,
)
try:
+17 -3
View File
@@ -112,6 +112,22 @@ def generate_token(
if color is None:
color = generate_color(identity)
# Build participant attributes — these are server-signed in the JWT
# and visible to all participants in the room.
attributes = {
"color": color,
"room_admin": "true" if is_admin_or_owner else "false",
"is_authenticated": "true" if not user.is_anonymous else "false",
}
# Add identity info for authenticated users (visible to other participants
# for identity verification in encrypted rooms)
if not user.is_anonymous:
if user.email:
attributes["email"] = user.email
if user.sub:
attributes["suite_user_id"] = str(user.sub)
token = (
AccessToken(
api_key=settings.LIVEKIT_CONFIGURATION["api_key"],
@@ -120,9 +136,7 @@ def generate_token(
.with_grants(video_grants)
.with_identity(identity)
.with_name(username or default_username)
.with_attributes(
{"color": color, "room_admin": "true" if is_admin_or_owner else "false"}
)
.with_attributes(attributes)
)
return token.to_jwt()
+11
View File
@@ -808,6 +808,17 @@ class Base(Configuration):
environ_prefix=None,
)
# End-to-end encryption settings
ENCRYPTION_ENABLED = values.BooleanValue(
False, environ_name="ENCRYPTION_ENABLED", environ_prefix=None
)
ENCRYPTION_VAULT_URL = values.Value(
None, environ_name="ENCRYPTION_VAULT_URL", environ_prefix=None
)
ENCRYPTION_INTERFACE_URL = values.Value(
None, environ_name="ENCRYPTION_INTERFACE_URL", environ_prefix=None
)
# External Applications
APPLICATION_CLIENT_ID_LENGTH = values.PositiveIntegerValue(
40,