Skip to content

test(security): R-5b — every use of ALL_WORKSPACES carries a reason - #528

Merged
jwvanderstam merged 4 commits into
mainfrom
test/r5b-all-workspaces-reasons
Nov 11, 2026
Merged

jwvanderstam merged 4 commits into
mainfrom
test/r5b-all-workspaces-reasons

Conversation

@jwvanderstam

Copy link
Copy Markdown
Owner

What

QP-9 review finding R-5, its open half (R-5b): a ratchet over the ALL_WORKSPACES call sites, each with a recorded reason.

Change

tests/unit/test_all_workspaces_has_a_reason.py walks the AST of src/ and collects every function (module:qualname) that passes ALL_WORKSPACES as a value. Comparisons (scope is ALL_WORKSPACES) are branches, not uses, and are left out. The test fails on:

  • a function that passes it without an entry in REASONS;
  • an entry whose function no longer passes it.

No src/ change.

The audit behind the 42 reasons

Each function was checked against every caller that can reach it, not only the obvious one. They fall into these groups:

Group Examples
Boot and background work, no request connector registry load, SyncWorker interval/status/re-ingest, mark_ingested, the reranker export
A row that names its own workspace (ALL only for a NULL-workspace row) connectors.create, conversations.create*, memories.insert, ingest_document, documents.exists
Ids already obtained or proved in the caller's scope feedback.insert (R-8), memories.record_use, mark_extracted, _touch, contributions.archive
Authentication and authorisation, which establish the scope workspace_keys.resolve, members.role/for_user, an admin naming no workspace, the webhook receiver (id + mandatory secret)
Membership changes after the route's guard members.add/remove
Global-admin only the five purges and purge_all, user and workspace lists, feedback stats, GKB contribute and list, admin stats (R-6)
Errs toward the safe answer any_local_only_sources, which can only block the cloud fallback

Nothing was found that lets a workspace user reach another workspace's rows. Two observations, both outside this PR:

  • WorkspacesService.visible_to and the services' _actor() helpers still special-case a user id of "anonymous", the RBAC bypass's identity. No guard produces it any more, so the branch is dead code. Left as is (pre-existing).
  • feedback.insert writes answer_feedback without a workspace_id. Not a leak, since the ids are checked first (R-8), but the row carries no workspace of its own.

Tests

  • 3 tests pass on main's src/.
  • A probe module adding an unreasoned ALL_WORKSPACES use makes the first test fail with ['zz_probe.py:leak'].
  • ruff is clean. test_docs_reference_what_exists passes with the new references.

Docs

SECURITY.md R-5 row, CHANGELOG, ROADMAP (item 4 ✅), architecture.md (the rule now points at the test), file-map. CHANGELOG and ROADMAP will need a trivial rebase against the other open QP-10 PRs.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KkWPqAcWbxfqz1aCP6Qsdx

Copilot AI balanced review requested due to automatic review settings November 8, 2026 20:11

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

jwvanderstam added a commit that referenced this pull request Nov 8, 2026
… uses

From the separate review of #528: the walk covered src/ only and matched only
the bare name. It now walks every directory whose code imports src (src/,
mcp_servers/, plugins/), counts scope.ALL_WORKSPACES, and fails on an import
that renames it. The two MCP list_sources tools get reasons. Three reasons
are corrected: the purge reason no longer cites R-9 (not on main yet),
feedback.insert says correct_doc_ids is unchecked, and for_user and
any_local_only_sources name the rest of what they do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkWPqAcWbxfqz1aCP6Qsdx
Copilot AI balanced review requested due to automatic review settings November 8, 2026 20:41

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@jwvanderstam

Copy link
Copy Markdown
Owner Author

Separate security review (CONTRIBUTING / QP-9)

A separate agent session reviewed this PR read-only against main. No finding at Medium or above. All 42 original reasons hold in the sense that matters: no non-admin and no workspace API key reaches any of them with another workspace's id or filename. A non-admin's scope is always pinned by check_workspace_access, so every workspace_id or ALL_WORKSPACES fallback sees None only for a global admin who named no workspace, or for a background job.

Low findings and how they were answered:

Finding Answer
The walk covered src/ only and matched the bare name only. Two MCP list_sources tools pass ALL_WORKSPACES unrecorded, and scope.ALL_WORKSPACES or a renamed import would escape. Fixed in b61d277. The walk now covers src/, mcp_servers/ and plugins/, counts the attribute form, and fails on a renamed import. Both MCP tools have reasons. Proven with a probe module, which turned two tests red.
_ADMIN_PURGE cited R-9's retired-first check, which is not on main yet (#526). Fixed: the reason no longer depends on merge order.
feedback.insert's reason was overstated: correct_doc_ids is stored unchecked, and the row carries no workspace_id. Fixed in the reason. Nothing reads correct_doc_ids today. Writing workspace_id and checking correct_doc_ids would be a follow-up, not this test-only PR.
documents.exists(filename, None) also matches global-tier contributions (workspace_id IS NULL). An admin upload with no workspace, an admin's workspace-less connector, or the SyncWorker's re-ingest (stale returns contributions) could update a contribution in place. Recorded for a follow-up (admin and worker only). Fix: AND contributed_at IS NULL in exists, and exclude contributions from stale.
The ratchet pins the function, not its callers: a new non-admin route calling an admin-only function would stay green. Accepted, and now stated in the test's docstring. The guard's tests, the permission matrix (docs/PERMISSIONS.md and its IVP) and the over-the-wire authorisation test are what catch that.
Info: any_local_only_sources leaks one bit across workspaces (errs safe). export_pairs trains one shared reranker on every workspace's text, and its JOIN chunk_stats ON TRUE is a correctness bug. receive_webhook reveals whether a connector id exists before the secret is checked. The first is in its reason now. The other two are recorded for follow-ups.

🤖 Generated with Claude Code

jwvanderstam and others added 2 commits November 8, 2026 22:34
ALL_WORKSPACES is the one scope row-level security does not narrow, and both
High findings of the QP-9 review were paths that passed it where a workspace
user could reach them. A unit test now walks src/ and fails on a function
that passes it without a recorded reason, and on a reason whose function no
longer passes it. A comparison on the scope is a branch, not a use.

The 42 functions that pass it today were each checked against every caller
that can reach them: boot and background work, rows that name their own
workspace, ids already proved in the caller's scope, the authorisation path
itself, and admin-only routes. None let a workspace user reach another
workspace's rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkWPqAcWbxfqz1aCP6Qsdx
… uses

From the separate review of #528: the walk covered src/ only and matched only
the bare name. It now walks every directory whose code imports src (src/,
mcp_servers/, plugins/), counts scope.ALL_WORKSPACES, and fails on an import
that renames it. The two MCP list_sources tools get reasons. Three reasons
are corrected: the purge reason no longer cites R-9 (not on main yet),
feedback.insert says correct_doc_ids is unchecked, and for_user and
any_local_only_sources name the rest of what they do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkWPqAcWbxfqz1aCP6Qsdx
@jwvanderstam
jwvanderstam force-pushed the test/r5b-all-workspaces-reasons branch from b61d277 to c4149bd Compare November 8, 2026 21:35
Copilot AI balanced review requested due to automatic review settings November 8, 2026 21:35

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

jwvanderstam added a commit that referenced this pull request Nov 11, 2026
…t a workspace (#532)

A global contribution and a document uploaded without a workspace both have
workspace_id NULL. documents.exists(filename, None) matched both, so an admin
upload naming no workspace, an admin's workspace-less connector, or the
re-ingest worker could update a contribution in place, and every workspace
reads the tier. documents.stale() also returned contributions, which have no
source file to re-read. Both now skip rows with contributed_at set.

Found by the separate review of #528. The integration test fails on the old
code with (True, True).


Claude-Session: https://claude.ai/code/session_01KkWPqAcWbxfqz1aCP6Qsdx

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts were side-by-side additions to CHANGELOG, file-map and the docs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings November 11, 2026 00:08

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

jwvanderstam added a commit that referenced this pull request Nov 11, 2026
…me 403 (#534)

POST /api/connectors/{id}/webhook is public, and answered 404 for an unknown
connector and 400 for one of another type before checking the secret, so an
anonymous caller could learn which connector ids exist and which are webhooks.
A missing connector, another type, a connector without a secret and a wrong
secret now all return the same 403.

Found by the separate review of #528. The parametrised test fails on the old
code for the missing and wrong-type cases.


Claude-Session: https://claude.ai/code/session_01KkWPqAcWbxfqz1aCP6Qsdx

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings November 11, 2026 01:18

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@sonarqubecloud

Copy link
Copy Markdown

@jwvanderstam
jwvanderstam merged commit af711f2 into main Nov 11, 2026
17 checks passed
@jwvanderstam
jwvanderstam deleted the test/r5b-all-workspaces-reasons branch November 11, 2026 02:02
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