Skip to content

fix: scope unscoped proposal and test-run lists to the caller's projects (#482) - #489

Merged
JabbaKadabra merged 1 commit into
masterfrom
fix/issue-482-unscoped-list-accessible-rows
Jul 28, 2026
Merged

fix: scope unscoped proposal and test-run lists to the caller's projects (#482)#489
JabbaKadabra merged 1 commit into
masterfrom
fix/issue-482-unscoped-list-accessible-rows

Conversation

@JabbaKadabra

Copy link
Copy Markdown
Collaborator

Summary

GET /api/proposals and GET /api/test-runs were the last two list endpoints reading GetAccessibleProjectIdsAsync directly and demanding a filter — a non-admin sending neither projectId nor agentId got a successful 200 with an empty list instead of the rows of the projects they can see. The eight controllers named in the issue were already fixed by #484 (which closed no issue, so #482 stayed open); these two were not. Both now resolve a scope through ResolveListScopeAsync like every other list endpoint and apply it inside the query.

Closes #482

Changes

  • ProposalsController — replaces the CanListAsync "return nothing" helper with the ListScopeAsync shape TestRunGroupsController already uses. Side effect worth noting: a named agentId outside the requested projectId no longer bypasses that project filter.
  • TestRunsController — an unfiltered list now resolves ResolveListScopeAsync(requestedProjectId: null) instead of short-circuiting every non-admin to an empty page.
  • IOptimizationProposalRepository.GetByProjectsAsync and ITestRunRepository.GetByProjectsPagedAsync — set-aware lookups so the scope is an IN predicate in SQL and the page is computed over the union, not merged from per-project pages. GetByProjectAsync now delegates to the set overload.
  • Regression tests at both layers: controller tests for own-project / multi-project-union / no-accessible-projects, and storage tests for the new repository methods (including the includeSystem behaviour for runs).
  • docs/architecture.md — documents the related-entity (agentId/suiteId) narrowing shape and the no-projectId-parameter case; the "no controller re-implements the check" claim is now actually true.
  • manual/admin/providers-and-api-keys.md + CHANGELOG.md — extend the existing Unscoped list endpoints return an empty page instead of the caller's accessible rows for non-admins #482 entry rather than duplicating it.

Verification

  • build: dotnet build Proxytrace.sln — 24 projects, 0 errors, 0 warnings
  • tests: full backend suite (dotnet test Proxytrace.sln) — 2,797 passed, 0 failed, 6 skipped. Ran the full suite because the change alters two shared repository interfaces. The four new controller tests were confirmed to fail with the controller fix reverted.
  • docs/changelog: docs/architecture.md, manual/admin/providers-and-api-keys.md (npm run docs:build green), CHANGELOG.md [Unreleased]. No UI strings, so no i18n extract.

Not done

No perf metric was added. The high-volume path for multi-project scope is already covered by agentCallsListByProjects; TestRunEntity paging has never had perf coverage (GetAllPagedAsync/GetByAgentPagedAsync included), and the new query is the same join shape as the existing TestRunGroupRepository.GetByProjectsPagedAsync with IN instead of =. Adding one would need the perf seeder to build run chains at scale — worth doing, but a separate change.

🤖 Generated with Claude Code

…projects

`GET /api/proposals` and `GET /api/test-runs` were the two list endpoints still
reading `GetAccessibleProjectIdsAsync` directly and *demanding* a filter: a
non-admin who sent neither `projectId` nor `agentId` got a successful response
with an empty list, rather than the rows of the projects they can actually see.
The callers that hit this are the ones with no reason to send a filter — a REST
API key, confined to one project, and integrations driving `/api/*`.

Both now resolve a scope through `ResolveListScopeAsync` like every other list
endpoint, and apply it in the query via new set-aware repository methods so the
page is computed over the union rather than merged after the fact.
`ProposalsController` gains the `ListScopeAsync` shape `TestRunGroupsController`
already uses, which also tightens a case it got wrong before: a named `agentId`
outside the requested project no longer bypasses that project filter.

This makes the claim already in docs/architecture.md — that no controller
re-implements the check — true.

Refs #482

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@JabbaKadabra
JabbaKadabra merged commit 86f72d8 into master Jul 28, 2026
12 checks passed
@JabbaKadabra
JabbaKadabra deleted the fix/issue-482-unscoped-list-accessible-rows branch July 28, 2026 18:04
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.

Unscoped list endpoints return an empty page instead of the caller's accessible rows for non-admins

1 participant