Skip to content

fix(backend): filter in-memory invocations by owner in list_wikis - #351

Merged
arozumenko merged 4 commits into
mainfrom
fix/list-wikis-ownership-leak
May 22, 2026
Merged

fix(backend): filter in-memory invocations by owner in list_wikis#351
arozumenko merged 4 commits into
mainfrom
fix/list-wikis-ownership-leak

Conversation

@arozumenko

Copy link
Copy Markdown
Owner

Summary

  • Users could see other people's wikis being built in their dashboard (status cards showing progress), then after completion the wiki remained in the list as an inaccessible entry (opening it returned 404)
  • Root cause: WikiManagementService.list_wikis (DB query) correctly filters by owner_id / visibility, but the second loop in the list_wikis route that appends active in-memory invocations iterated all running jobs with no ownership check
  • Fix: skip any invocation owned by a different user, mirroring the DB rule. Legacy unowned invocations (owner_id=="") remain visible to all. Shared visibility is not tracked on the Invocation model (in-progress wikis have no visibility yet); completed shared wikis reach the list via the DB path which already handles them correctly.

Test plan

  • Start a wiki generation as User A
  • Log in as User B — the in-progress card should not appear in User B's dashboard
  • After User A's wiki completes — it should not appear in User B's list (unless set to shared)
  • Legacy wikis with empty owner_id still appear for all users

🤖 Generated with Claude Code

The DB query in WikiManagementService.list_wikis correctly filters by
owner_id / visibility, but the loop that appends active in-memory
invocations iterated all invocations with no ownership check, leaking
other users' in-progress and failed builds into every user's wiki list
(visible in the dashboard, 404 when opened).

Mirror the same rule: skip invocations owned by someone other than the
caller. Legacy unowned invocations (owner_id=="") stay visible to all.
Shared visibility is not tracked on Invocation (in-progress wikis have
no visibility yet); completed shared wikis reach the list via the DB
path which already handles them correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 22, 2026 17:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a dashboard data-leak where GET /api/v1/wikis could include in-progress (in-memory) invocations belonging to other users, resulting in visible-but-inaccessible wiki cards. The change aligns the in-memory invocation append logic with the DB-backed visibility/ownership rules already enforced by WikiManagementService.list_wikis.

Changes:

  • Added ownership filtering when appending active/failed in-memory invocations to the wiki list response.
  • Documented the intended visibility behavior for in-memory invocations (own + legacy unowned).

Comment thread backend/app/api/routes.py
Comment on lines 441 to +443
for inv in service.invocations.values():
if inv.owner_id and user_id and inv.owner_id != user_id:
continue
Comment thread backend/app/api/routes.py Outdated
Comment on lines +439 to +442
# Owned invocations from other users are never shown — the caller would
# get a 404 trying to open them, so leaking them in the list is wrong.
for inv in service.invocations.values():
if inv.owner_id and user_id and inv.owner_id != user_id:
…_wikis leak

- Normalize user_id with `or None` so an empty-string user.id (JWT missing
  sub) doesn't bypass the filter
- Change guard from `and user_id` to `not user_id or inv.owner_id != user_id`
  so callers with no valid user_id also can't see other users' invocations
  (mirrors DB rule: anonymous sees only shared + legacy unowned)
- Add two regression tests: one verifying the ownership split across three
  invocations (own / other-user / legacy), one covering the empty-string
  user_id edge case

Flaky test note: test_generate_chapter::test_oversized_content_is_kept_not_split
fails intermittently in CI under -n auto because generate_chapter runs
sub-pages concurrently via asyncio.gather + run_in_executor, making
SequentialFakeLLM response order non-deterministic. Pre-existing; unrelated
to this change.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment on lines +523 to +524
"own": _make_foreign_invocation("wiki-own", owner_id="user-1"),
"other": _make_foreign_invocation("wiki-other", owner_id="user-2"),
Comment on lines +531 to +534
returned_ids = {w["wiki_id"] for w in resp.json()["wikis"]}
assert "wiki-own" in returned_ids, "caller's own invocation must be included"
assert "wiki-legacy" in returned_ids, "legacy unowned invocation must be included"
assert "wiki-other" not in returned_ids, "other user's invocation must be excluded"
Comment on lines +551 to +554
mock_service.invocations = {
"other": _make_foreign_invocation("wiki-other", owner_id="user-2"),
"legacy": _make_foreign_invocation("wiki-legacy", owner_id=""),
}
Remove manual alignment spaces flagged by Ruff E221/E241/E272.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Comment thread backend/app/api/routes.py
Comment on lines +398 to 399
user_id = (user.id or None) if user else None
result = await management.list_wikis(user_id=user_id)
Comment thread backend/app/api/routes.py
Comment on lines +437 to +443
# Add active/failed invocations not yet in completed list.
# Mirror the DB visibility rule: own wikis + legacy unowned (owner_id=="").
# Owned invocations from other users are never shown — the caller would
# get a 404 trying to open them, so leaking them in the list is wrong.
for inv in service.invocations.values():
if inv.owner_id and (not user_id or inv.owner_id != user_id):
continue
…t_wikis

- Normalize user_id with `or None` in all routes (not just list_wikis) so
  an empty-string user.id can never bypass ownership checks
- Fix get_wiki B2 redaction: change `and user_id and` to
  `and (not user_id or ...)` so the same guard pattern is used everywhere
- Fix best_inv enrichment loop in list_wikis: skip invocations owned by
  other users when selecting status/progress to attach to a shared/legacy
  wiki, matching the B2 behaviour already in get_wiki

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

@arozumenko
arozumenko merged commit 3f5a444 into main May 22, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants