diff --git a/src/inkwell/notes/__init__.py b/src/inkwell/notes/__init__.py index c913bf7..77d447e 100644 --- a/src/inkwell/notes/__init__.py +++ b/src/inkwell/notes/__init__.py @@ -387,7 +387,9 @@ async def reorder_notes(): for n in ( await db.scalars( select(Note).where( - Note.id.in_(parsed), visible_to_user("note", Note.owner_id, Note.id, g.user_id) + Note.id.in_(parsed), + visible_to_user("note", Note.owner_id, Note.id, g.user_id), + Note.purged_at.is_(None), ) ) ).all() @@ -431,13 +433,8 @@ async def create_note(): @bp.get("/") @login_required async def get_note(note_id: str): - nid = parse_uuid(note_id) - if nid is None: - return not_found() async with session_scope() as db: - note = await db.scalar( - select(Note).where(Note.id == nid, visible_to_user("note", Note.owner_id, Note.id, g.user_id)) - ) + note = await _get_visible(db, note_id) if note is None: return not_found() return jsonify(await _serialize_note(db, note, g.user_id)) @@ -653,18 +650,17 @@ async def upload_attachment(note_id: str): @bp.get("//attachments/") @login_required async def get_attachment(note_id: str, att_id: str): - nid = parse_uuid(note_id) aid = parse_uuid(att_id) - if nid is None or aid is None: + if aid is None: return not_found() async with session_scope() as db: - # Owner OR shared may view (rule 47). - note = await db.scalar( - select(Note).where(Note.id == nid, visible_to_user("note", Note.owner_id, Note.id, g.user_id)) - ) + # Owner OR shared may view (rule 47). A purged note's files are gone with it. + note = await _get_visible(db, note_id) if note is None: return not_found() - att = await db.scalar(select(NoteAttachment).where(NoteAttachment.id == aid, NoteAttachment.note_id == nid)) + att = await db.scalar( + select(NoteAttachment).where(NoteAttachment.id == aid, NoteAttachment.note_id == note.id) + ) if att is None: return not_found() mime = att.mime diff --git a/tests/test_integration.py b/tests/test_integration.py index caa3a88..d31d500 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -891,6 +891,22 @@ async def test_an_offline_attachment_uploads_once_under_its_own_id(app_client, d assert (await _put(app_client, aid, other, b"%PDF-1")).status_code == 409, "one id, one note" +async def test_a_purged_note_and_its_files_read_as_gone(app_client, db): + """A purged note reads as absent (#2128). Opening it and its attachment selected + with the ACL inline and skipped that filter, so a tombstone came back 200 (#5383).""" + await _signed_in(app_client, "purged") + nid = await _pushed_note(app_client) + aid = str(uuid.uuid4()) + assert (await _put(app_client, aid, nid, b"%PDF-1")).status_code == 201 + assert (await app_client.get(f"/api/notes/{nid}/attachments/{aid}")).status_code == 200 + + assert (await app_client.post(f"/api/notes/{nid}/trash")).status_code == 200 + assert (await app_client.delete(f"/api/notes/{nid}")).status_code == 200 + + assert (await app_client.get(f"/api/notes/{nid}")).status_code == 404 + assert (await app_client.get(f"/api/notes/{nid}/attachments/{aid}")).status_code == 404 + + async def test_an_upload_to_a_note_the_caller_does_not_own_is_not_found(app_client, db): # Signed in first: registration is open only to the first account. await _signed_in(app_client, "intruder")