Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions backend/routers/conversations.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import asyncio

Check warning on line 1 in backend/routers/conversations.py

View workflow job for this annotation

GitHub Actions / PR Metadata Preflight

Large changed file

backend/routers/conversations.py is 1324 lines; consider splitting files over 800 lines.

Check warning on line 1 in backend/routers/conversations.py

View workflow job for this annotation

GitHub Actions / PR Metadata Preflight

Large changed file

backend/routers/conversations.py is 1324 lines; consider splitting files over 800 lines.

from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks
from typing import Any, Dict, List, Optional
Expand Down Expand Up @@ -1118,7 +1118,14 @@
if not visibility or visibility == ConversationVisibility.private:
raise HTTPException(status_code=404, detail="Conversation is private")
conversation = deserialize_conversation(conversation)
# This endpoint is public and unauthenticated. Strip fields that are internal
# to the owner and never part of the shared transcript/summary the user chose
# to publish: precise geolocation, the server-side encryption tier, and
# external_data (which carries merge provenance — other conversation ids — and
# integration metadata).
conversation.geolocation = None
conversation.data_protection_level = None
conversation.external_data = None

# Fetch people data for speaker names
person_ids = conversation.get_person_ids()
Expand Down
44 changes: 44 additions & 0 deletions backend/tests/unit/test_shared_conversation_field_exposure.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""The public /v1/conversations/{id}/shared endpoint must not expose owner-internal fields.

The endpoint returns a conversation to anyone with the link (no auth). It is
meant to publish the transcript/summary the owner shared, not internal fields:
precise geolocation (already stripped), the server-side encryption tier
(`data_protection_level`), or `external_data` (merge provenance — other
conversation ids — and integration metadata).
"""

from types import SimpleNamespace
from unittest.mock import patch

import routers.conversations as conv_router


def test_shared_endpoint_strips_internal_fields_before_serialising():
conv = SimpleNamespace(
geolocation='here',
data_protection_level='enhanced',
external_data={'merge_metadata': {'source_ids': ['other-conv']}},
)
conv.get_person_ids = lambda: []

captured = {}

def fake_to_dict(c):
captured['geolocation'] = c.geolocation
captured['data_protection_level'] = c.data_protection_level
captured['external_data'] = c.external_data
return {'id': 'c1'}

with patch.object(conv_router.redis_db, 'get_conversation_uid', return_value='owner-uid'), patch.object(
conv_router, '_get_valid_conversation_by_id', return_value={'visibility': 'public'}
), patch.object(conv_router, 'deserialize_conversation', return_value=conv), patch.object(
conv_router, 'conversation_to_dict', side_effect=fake_to_dict
), patch.object(
conv_router.users_db, 'get_people_by_ids', return_value=[]
):
conv_router.get_shared_conversation_by_id('c1')

# All three owner-internal fields must be cleared before serialization.
assert captured['geolocation'] is None
assert captured['data_protection_level'] is None
assert captured['external_data'] is None
Loading