"""Pin owner-scoping of the autonomous email->calendar event snapshot. The email auto-calendar pass fans out over EVERY user's mailbox and used to feed an *unscoped* upcoming-events snapshot to the extraction LLM, then execute the model's create/update/delete ops via do_manage_calendar with owner=None — so processing one tenant's mail could read AND mutate another tenant's calendar (and leak every tenant's event titles to the LLM endpoint). The fix routes the snapshot through core.database.get_upcoming_events(owner) and passes the account owner to do_manage_calendar. This test pins that get_upcoming_events scopes to the owner; it fails if the owner filter is dropped (the original cross-tenant behavior). """ import ast import asyncio import sys import types from pathlib import Path from types import SimpleNamespace from unittest.mock import MagicMock import pytest from fastapi import HTTPException def test_get_upcoming_events_is_owner_scoped(): source = Path("core/database.py").read_text() tree = ast.parse(source) fn = next( node for node in tree.body if isinstance(node, ast.FunctionDef) and node.name == "get_upcoming_events" ) body = ast.unparse(fn) assert "join(CalendarCal)" in body assert "if owner is not None:" in body assert "q.filter(CalendarCal.owner == owner)" in body class _Expr: def __init__(self, op, field=None, value=None, children=()): self.op = op self.field = field self.value = value self.children = tuple(children) def __or__(self, other): return _Expr("or", children=(self, other)) def __and__(self, other): return _Expr("and", children=(self, other)) class _Column: def __init__(self, field): self.field = field def __eq__(self, value): return _Expr("eq", self.field, value) def __ne__(self, value): return _Expr("ne", self.field, value) def __lt__(self, value): return _Expr("lt", self.field, value) def __gt__(self, value): return _Expr("gt", self.field, value) def is_(self, value): return _Expr("is", self.field, value) def isnot(self, value): return _Expr("isnot", self.field, value) def _expr_contains(expr, field, value): if isinstance(expr, _Expr): if expr.field == field and expr.value == value: return True return any(_expr_contains(child, field, value) for child in expr.children) return False class _CalendarCal: id = _Column("CalendarCal.id") owner = _Column("CalendarCal.owner") name = _Column("CalendarCal.name") class _CalendarEvent: uid = _Column("CalendarEvent.uid") status = _Column("CalendarEvent.status") rrule = _Column("CalendarEvent.rrule") dtstart = _Column("CalendarEvent.dtstart") dtend = _Column("CalendarEvent.dtend") calendar_id = _Column("CalendarEvent.calendar_id") class _FakeQuery: def __init__(self, rows): self.rows = rows self.filter_calls = [] self.owner_filter = None self.all_called = False def join(self, *_args, **_kwargs): return self def filter(self, *exprs): self.filter_calls.append(exprs) for expr in exprs: if _expr_contains(expr, "CalendarCal.owner", "alice"): self.owner_filter = "alice" return self def order_by(self, *_args, **_kwargs): return self def first(self): return self.rows[0] if self.rows else None def all(self): self.all_called = True if self.owner_filter is None: return list(self.rows) return [ row for row in self.rows if getattr(getattr(row, "calendar", None), "owner", None) == self.owner_filter ] class _FakeSession: def __init__(self, *, calendars=(), events=()): self.calendar_query = _FakeQuery(list(calendars)) self.event_query = _FakeQuery(list(events)) self.add = MagicMock() self.commit = MagicMock() self.rollback = MagicMock() self.close = MagicMock() def query(self, model): if model is _CalendarCal: return self.calendar_query if model is _CalendarEvent: return self.event_query raise AssertionError(f"unexpected query model: {model!r}") def _install_calendar_db_stub(monkeypatch): db = types.ModuleType("core.database") db.SessionLocal = MagicMock() db.CalendarCal = _CalendarCal db.CalendarEvent = _CalendarEvent for name in [ "Base", "Document", "DocumentVersion", "Session", "ChatMessage", "GalleryImage", "GalleryAlbum", "Note", "ScheduledTask", "TaskRun", "ModelEndpoint", "Webhook", ]: setattr(db, name, MagicMock()) monkeypatch.setitem(sys.modules, "core.database", db) return db def _install_multipart_stub(monkeypatch): multipart = types.ModuleType("python_multipart") multipart.__version__ = "0.0.20" monkeypatch.setitem(sys.modules, "python_multipart", multipart) def _import_calendar_routes(monkeypatch): _install_calendar_db_stub(monkeypatch) _install_multipart_stub(monkeypatch) monkeypatch.delitem(sys.modules, "routes.calendar_routes", raising=False) mod = __import__("routes.calendar_routes", fromlist=["setup_calendar_routes"]) monkeypatch.setattr(mod, "or_", lambda *args: _Expr("or", children=args)) monkeypatch.setattr(mod, "and_", lambda *args: _Expr("and", children=args)) return mod def _route_endpoint(calendar_routes, path, method): router = calendar_routes.setup_calendar_routes() full_path = f"/api/calendar{path}" for route in router.routes: if route.path == full_path and method in route.methods: return route.endpoint raise AssertionError(f"route not found: {method} {full_path}") def _request(user="alice"): return SimpleNamespace(state=SimpleNamespace(current_user=user)) def _calendar(owner, cal_id="cal-target"): return SimpleNamespace(id=cal_id, owner=owner, name=f"{owner or 'null'} calendar") def _event(owner, uid): return SimpleNamespace( uid=uid, calendar=_calendar(owner, cal_id=f"{owner or 'null'}-cal"), calendar_id=f"{owner or 'null'}-cal", dtstart=SimpleNamespace(isoformat=lambda: f"{uid}-start"), dtend=SimpleNamespace(isoformat=lambda: f"{uid}-end"), summary=uid, description="", location="", all_day=False, is_utc=False, rrule="", color=None, event_type=None, importance="normal", ) def test_create_event_rejects_null_owner_calendar_href_at_route_boundary(monkeypatch): calendar_routes = _import_calendar_routes(monkeypatch) session = _FakeSession(calendars=[_calendar(None)]) monkeypatch.setattr(calendar_routes, "SessionLocal", lambda: session) create_event = _route_endpoint(calendar_routes, "/events", "POST") with pytest.raises(HTTPException) as exc: asyncio.run(create_event( _request(), calendar_routes.EventCreate( summary="blocked", dtstart="2026-06-02T10:00:00", calendar_href="cal-target", ), )) assert exc.value.status_code == 404 session.add.assert_not_called() session.commit.assert_not_called() session.close.assert_called_once() def test_create_event_rejects_cross_owner_calendar_href_at_route_boundary(monkeypatch): calendar_routes = _import_calendar_routes(monkeypatch) session = _FakeSession(calendars=[_calendar("bob")]) monkeypatch.setattr(calendar_routes, "SessionLocal", lambda: session) create_event = _route_endpoint(calendar_routes, "/events", "POST") with pytest.raises(HTTPException) as exc: asyncio.run(create_event( _request(), calendar_routes.EventCreate( summary="blocked", dtstart="2026-06-02T10:00:00", calendar_href="cal-target", ), )) assert exc.value.status_code == 404 session.add.assert_not_called() session.commit.assert_not_called() session.close.assert_called_once() def test_list_events_filters_by_calendar_owner_before_output(monkeypatch): calendar_routes = _import_calendar_routes(monkeypatch) session = _FakeSession(events=[ _event(None, "null-owner"), _event("bob", "bob-event"), _event("alice", "alice-event"), ]) monkeypatch.setattr(calendar_routes, "SessionLocal", lambda: session) expanded = [] def fake_expand(event, _start, _end): assert event.calendar.owner == "alice" expanded.append(event.uid) return [{"uid": event.uid, "dtstart": "2026-06-02T10:00:00"}] monkeypatch.setattr(calendar_routes, "_expand_rrule", fake_expand) list_events = _route_endpoint(calendar_routes, "/events", "GET") out = asyncio.run(list_events( _request(), start="2026-06-01T00:00:00", end="2026-06-03T00:00:00", )) assert out == {"events": [{"uid": "alice-event", "dtstart": "2026-06-02T10:00:00"}]} assert expanded == ["alice-event"] assert session.event_query.owner_filter == "alice" session.close.assert_called_once() def test_export_ics_rejects_null_owner_calendar_at_route_boundary(monkeypatch): calendar_routes = _import_calendar_routes(monkeypatch) session = _FakeSession(calendars=[_calendar(None)]) monkeypatch.setattr(calendar_routes, "SessionLocal", lambda: session) export_ics = _route_endpoint(calendar_routes, "/export/{cal_id}", "GET") with pytest.raises(HTTPException) as exc: asyncio.run(export_ics(_request(), cal_id="cal-target")) assert exc.value.status_code == 404 assert not session.event_query.all_called session.close.assert_called_once() def test_export_ics_rejects_cross_owner_calendar_at_route_boundary(monkeypatch): calendar_routes = _import_calendar_routes(monkeypatch) session = _FakeSession(calendars=[_calendar("bob")]) monkeypatch.setattr(calendar_routes, "SessionLocal", lambda: session) export_ics = _route_endpoint(calendar_routes, "/export/{cal_id}", "GET") with pytest.raises(HTTPException) as exc: asyncio.run(export_ics(_request(), cal_id="cal-target")) assert exc.value.status_code == 404 assert not session.event_query.all_called session.close.assert_called_once() def test_export_ics_sanitizes_calendar_name_for_download_header(monkeypatch): calendar_routes = _import_calendar_routes(monkeypatch) cal = _calendar("alice") cal.name = 'Work\r\nX-Injected: yes";/..\\evil' session = _FakeSession(calendars=[cal]) monkeypatch.setattr(calendar_routes, "SessionLocal", lambda: session) export_ics = _route_endpoint(calendar_routes, "/export/{cal_id}", "GET") response = asyncio.run(export_ics(_request(), cal_id="cal-target")) assert ( response.headers["content-disposition"] == 'attachment; filename="Work__X-Injected__yes___.._evil.ics"' ) assert response.headers["x-content-type-options"] == "nosniff" session.close.assert_called_once()