mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-17 10:15:27 -04:00
fix: check-in calendar digest leaks every user's events (missing owner scope) (#1925)
* fix: check-in calendar digest leaks every user's events (no owner scope) * Seed dtend on calendar events in digest test so the NOT NULL column is satisfied
This commit is contained in:
+24
-5
@@ -236,6 +236,29 @@ def _digest_windows(now):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _checkin_calendar_events(db, owner, start, end):
|
||||||
|
"""Calendar events in [start, end] for ONE owner, for the check-in digest.
|
||||||
|
|
||||||
|
Ownership lives on CalendarCal.owner; events inherit it via calendar_id.
|
||||||
|
The digest query had no owner scope, so it pulled EVERY user's events into
|
||||||
|
one user's check-in (a cross-tenant leak of summaries/locations). Scope it
|
||||||
|
by joining CalendarCal, mirroring routes/calendar_routes.list_events.
|
||||||
|
"""
|
||||||
|
from core.database import CalendarEvent as _CE, CalendarCal as _CC
|
||||||
|
return (
|
||||||
|
db.query(_CE)
|
||||||
|
.join(_CC, _CE.calendar_id == _CC.id)
|
||||||
|
.filter(
|
||||||
|
_CC.owner == owner,
|
||||||
|
_CE.dtstart >= start,
|
||||||
|
_CE.dtstart <= end,
|
||||||
|
_CE.status != "cancelled",
|
||||||
|
)
|
||||||
|
.order_by(_CE.dtstart)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TaskScheduler:
|
class TaskScheduler:
|
||||||
def __init__(self, session_manager):
|
def __init__(self, session_manager):
|
||||||
self._session_manager = session_manager
|
self._session_manager = session_manager
|
||||||
@@ -1127,11 +1150,7 @@ class TaskScheduler:
|
|||||||
# Strip timezone for naive DB comparison
|
# Strip timezone for naive DB comparison
|
||||||
_s = start.replace(tzinfo=None) if start.tzinfo else start
|
_s = start.replace(tzinfo=None) if start.tzinfo else start
|
||||||
_e = end.replace(tzinfo=None) if end.tzinfo else end
|
_e = end.replace(tzinfo=None) if end.tzinfo else end
|
||||||
evs = _db.query(_CE).filter(
|
evs = _checkin_calendar_events(_db, task.owner, _s, _e)
|
||||||
_CE.dtstart >= _s,
|
|
||||||
_CE.dtstart <= _e,
|
|
||||||
_CE.status != "cancelled",
|
|
||||||
).order_by(_CE.dtstart).all()
|
|
||||||
if not evs:
|
if not evs:
|
||||||
continue
|
continue
|
||||||
# Group by importance for richer output
|
# Group by importance for richer output
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
"""Check-in calendar digest must be scoped to the task owner.
|
||||||
|
|
||||||
|
The digest query selected CalendarEvent with no owner scope, so a scheduled
|
||||||
|
check-in for one user pulled EVERY user's calendar events (summaries,
|
||||||
|
locations) into their digest — a cross-tenant leak. Ownership lives on
|
||||||
|
CalendarCal.owner; the query must join it, like routes/calendar_routes.
|
||||||
|
"""
|
||||||
|
import tempfile
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from sqlalchemy.pool import NullPool
|
||||||
|
|
||||||
|
import core.database as cdb
|
||||||
|
from core.database import CalendarEvent, CalendarCal
|
||||||
|
from src.task_scheduler import _checkin_calendar_events
|
||||||
|
|
||||||
|
_TMPDB = tempfile.NamedTemporaryFile(suffix=".db", delete=False)
|
||||||
|
_ENGINE = create_engine(f"sqlite:///{_TMPDB.name}", connect_args={"check_same_thread": False}, poolclass=NullPool)
|
||||||
|
cdb.Base.metadata.create_all(_ENGINE)
|
||||||
|
_TS = sessionmaker(bind=_ENGINE, autoflush=False, autocommit=False)
|
||||||
|
|
||||||
|
|
||||||
|
def _seed():
|
||||||
|
db = _TS()
|
||||||
|
try:
|
||||||
|
db.query(CalendarEvent).delete(); db.query(CalendarCal).delete()
|
||||||
|
db.add(CalendarCal(id="calA", owner="alice", name="A"))
|
||||||
|
db.add(CalendarCal(id="calB", owner="bob", name="B"))
|
||||||
|
db.add(CalendarEvent(uid="a1", calendar_id="calA", summary="Alice mtg",
|
||||||
|
dtstart=datetime(2026, 6, 10, 9, 0),
|
||||||
|
dtend=datetime(2026, 6, 10, 10, 0), status="confirmed"))
|
||||||
|
db.add(CalendarEvent(uid="b1", calendar_id="calB", summary="Bob secret",
|
||||||
|
dtstart=datetime(2026, 6, 10, 10, 0),
|
||||||
|
dtend=datetime(2026, 6, 10, 11, 0), status="confirmed"))
|
||||||
|
db.commit()
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_digest_only_returns_owner_events():
|
||||||
|
_seed()
|
||||||
|
db = _TS()
|
||||||
|
try:
|
||||||
|
s, e = datetime(2026, 6, 1), datetime(2026, 6, 30)
|
||||||
|
alice = _checkin_calendar_events(db, "alice", s, e)
|
||||||
|
assert [ev.summary for ev in alice] == ["Alice mtg"] # not Bob's
|
||||||
|
bob = _checkin_calendar_events(db, "bob", s, e)
|
||||||
|
assert [ev.summary for ev in bob] == ["Bob secret"]
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_cancelled_excluded_and_window_respected():
|
||||||
|
_seed()
|
||||||
|
db = _TS()
|
||||||
|
try:
|
||||||
|
db2 = _TS()
|
||||||
|
db2.add(CalendarEvent(uid="a2", calendar_id="calA", summary="cancelled",
|
||||||
|
dtstart=datetime(2026, 6, 11),
|
||||||
|
dtend=datetime(2026, 6, 11, 1, 0), status="cancelled"))
|
||||||
|
db2.commit(); db2.close()
|
||||||
|
s, e = datetime(2026, 6, 1), datetime(2026, 6, 30)
|
||||||
|
out = _checkin_calendar_events(db, "alice", s, e)
|
||||||
|
assert "cancelled" not in [ev.summary for ev in out]
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
Reference in New Issue
Block a user