Opening a purged note, or one of its files, is a 404
get_note and get_attachment selected with the ACL inline and skipped the purged filter, so a tombstone came back 200 (#2128 says a purged note reads as absent). Both now go through _get_visible, which cannot skip it. Reorder's batch lookup gains the same filter, so a stale id cannot write a place onto a tombstone. Fixes #5383. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -387,7 +387,9 @@ async def reorder_notes():
|
|||||||
for n in (
|
for n in (
|
||||||
await db.scalars(
|
await db.scalars(
|
||||||
select(Note).where(
|
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()
|
).all()
|
||||||
@@ -431,13 +433,8 @@ async def create_note():
|
|||||||
@bp.get("/<note_id>")
|
@bp.get("/<note_id>")
|
||||||
@login_required
|
@login_required
|
||||||
async def get_note(note_id: str):
|
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:
|
async with session_scope() as db:
|
||||||
note = await db.scalar(
|
note = await _get_visible(db, note_id)
|
||||||
select(Note).where(Note.id == nid, visible_to_user("note", Note.owner_id, Note.id, g.user_id))
|
|
||||||
)
|
|
||||||
if note is None:
|
if note is None:
|
||||||
return not_found()
|
return not_found()
|
||||||
return jsonify(await _serialize_note(db, note, g.user_id))
|
return jsonify(await _serialize_note(db, note, g.user_id))
|
||||||
@@ -653,18 +650,17 @@ async def upload_attachment(note_id: str):
|
|||||||
@bp.get("/<note_id>/attachments/<att_id>")
|
@bp.get("/<note_id>/attachments/<att_id>")
|
||||||
@login_required
|
@login_required
|
||||||
async def get_attachment(note_id: str, att_id: str):
|
async def get_attachment(note_id: str, att_id: str):
|
||||||
nid = parse_uuid(note_id)
|
|
||||||
aid = parse_uuid(att_id)
|
aid = parse_uuid(att_id)
|
||||||
if nid is None or aid is None:
|
if aid is None:
|
||||||
return not_found()
|
return not_found()
|
||||||
async with session_scope() as db:
|
async with session_scope() as db:
|
||||||
# Owner OR shared may view (rule 47).
|
# Owner OR shared may view (rule 47). A purged note's files are gone with it.
|
||||||
note = await db.scalar(
|
note = await _get_visible(db, note_id)
|
||||||
select(Note).where(Note.id == nid, visible_to_user("note", Note.owner_id, Note.id, g.user_id))
|
|
||||||
)
|
|
||||||
if note is None:
|
if note is None:
|
||||||
return not_found()
|
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:
|
if att is None:
|
||||||
return not_found()
|
return not_found()
|
||||||
mime = att.mime
|
mime = att.mime
|
||||||
|
|||||||
@@ -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"
|
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):
|
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.
|
# Signed in first: registration is open only to the first account.
|
||||||
await _signed_in(app_client, "intruder")
|
await _signed_in(app_client, "intruder")
|
||||||
|
|||||||
Reference in New Issue
Block a user