fix(webapp): a pending wiki suggestion can't be read by id (#836) #838

Merged
claude-bot merged 2 commits from fix/proposal-read-leak into main 2026-09-26 15:50:09 +00:00
Collaborator

Closes #836.

The bug: a lore proposal (LoreEntry.proposed_by_bot, cleared on approval) left out of the lists could still be read by any member who knew its id. Every lore read goes through get_lore_entry, which filtered only deleted_at.

The fix: _hide_pending_proposal(entry, is_gm) in routers/campaigns.py, applied to every read path a player can reach by id:

  • GET /lore/{id}: a non-GM gets 404 with the same body as a missing entry.
  • GET /lore/{id}/versions: the membership lookup moved ahead of the fetch, so a proposal is 404 rather than 403. A 403 would confirm it exists.
  • GET /lore/relationships?entry_id=: a hidden proposal gives the same empty list as an unrelated id.
  • Unchanged: the embedded fields (sessions, appearances, image) come through the single-entry response, and GM-only writes are already GM-only.

Tests: tests/test_proposal_read_leak.py, 11 tests.

  • Five of them failed before the fix: a player got 200 or 403, and the relationships list leaked.
  • With the lore modules, 120 pass.
  • ruff is clean.

🤖 Generated with Claude Code

Closes #836. **The bug:** a lore proposal (`LoreEntry.proposed_by_bot`, cleared on approval) left out of the lists could still be read by any member who knew its id. Every lore read goes through `get_lore_entry`, which filtered only `deleted_at`. **The fix:** `_hide_pending_proposal(entry, is_gm)` in `routers/campaigns.py`, applied to every read path a player can reach by id: - **`GET /lore/{id}`:** a non-GM gets 404 with the same body as a missing entry. - **`GET /lore/{id}/versions`:** the membership lookup moved ahead of the fetch, so a proposal is 404 rather than 403. A 403 would confirm it exists. - **`GET /lore/relationships?entry_id=`:** a hidden proposal gives the same empty list as an unrelated id. - **Unchanged:** the embedded fields (sessions, appearances, image) come through the single-entry response, and GM-only writes are already GM-only. **Tests:** `tests/test_proposal_read_leak.py`, 11 tests. - Five of them failed before the fix: a player got 200 or 403, and the relationships list leaked. - With the lore modules, 120 pass. - ruff is clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(webapp): a pending wiki proposal is not readable before a GM sees it (#836)
All checks were successful
CI / Backend lint (ruff) (pull_request) Successful in 51s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 55s
CI / Bot/backend version sync (pull_request) Successful in 27s
CI / Feedback site tests (pull_request) Successful in 1m9s
CI / Does this change touch the recording path? (pull_request) Successful in 32s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Bot tests and audit (pull_request) Successful in 3m5s
CI / Docker image build (pull_request) Successful in 2m17s
CI / Backend migration, tests, and audit (pull_request) Successful in 13m52s
CI / Frontend tests, audit, and build (pull_request) Successful in 17m52s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 17m25s
125ec0a9a4
`get_lore_entry`, the getter almost every lore endpoint funnels through, only
excluded trashed entries — not unaccepted bot proposals. A campaign member who
knew or guessed a proposal's id could read its full body via
GET /api/campaigns/{id}/lore/{entry_id} or its /versions history, and confirm
it exists, before any GM had reviewed it. The proposal lists already filtered
this correctly; the single-entry reads never did.

Add `_hide_pending_proposal`, applied to every id-keyed read that can reach a
lore entry: the single entry endpoint, its version history, and the approved
relationships list when filtered by entry_id. A pending proposal now looks
exactly like a missing id to a non-GM (same 404 body); a GM is unaffected.

Tests in test_proposal_read_leak.py confirmed the leak before the fix (5
failures: player reads returned 200/403 instead of 404, and the leaking
relationship result was non-empty) and pass after it, alongside the existing
lore/wiki suite (120 total).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Merge remote-tracking branch 'origin/main' into fix/proposal-read-leak
All checks were successful
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 59s
CI / Bot tests and audit (pull_request) Successful in 2m3s
CI / Backend lint (ruff) (pull_request) Successful in 37s
CI / Bot/backend version sync (pull_request) Successful in 26s
CI / Feedback site tests (pull_request) Successful in 1m2s
CI / Does this change touch the recording path? (pull_request) Successful in 35s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Docker image build (pull_request) Successful in 4m57s
CI / Backend migration, tests, and audit (pull_request) Successful in 31m1s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 21m8s
CI / Frontend tests, audit, and build (pull_request) Successful in 35m33s
f2591b9326
# Conflicts:
#	CHANGELOG.md
Sign in to join this conversation.
No description provided.