Skip to content

fix(agent): check the segment query permission wherever a live query segment is used - #2046

Open
Scra3 wants to merge 5 commits into
mainfrom
fix/prd-1759-check-segment-query-permission
Open

Scra3 wants to merge 5 commits into
mainfrom
fix/prd-1759-check-segment-query-permission

Conversation

@Scra3

@Scra3 Scra3 commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

fixes PRD-1759

Every route that builds its filter from a live query segment checks that the user may run that segment, on the collection whose filter carries it.

What changes

  1. The segment checked is the one the filter uses, whether it comes from the query string or the request body.
  2. Listing, counting and exporting records, related ones included, check the segment of the collection they list.
  3. Deleting records and associating or dissociating related records check the segment of the collection they change.
  4. Running an action, loading or changing its form, and approving it check the segment of the selection, for the approver too on an approval. A selection of explicit ids leaves its segment out: it is neither checked nor run, as in forest-express and forest-rails.
  5. A chart checks the segment of the collection it charts, and a count leaderboard no longer checks it against the related collection.
  6. Live query segments and SQL charts run in a transaction that is always rolled back, read-only on Postgres: nothing they run is ever committed, whatever the dialect (datasource-sequelize, so datasource-sql too).

Users allowed to run a segment keep every feature on it: each path above has a test for the allowed user next to the one for the refused user.

Decisions

Squash message

fix(agent): check the segment query permission wherever a live query segment is used

Each route that builds its filter from a live query segment checks that the user may run that
segment, on the collection whose filter carries it, whether the segment comes from the query
string or the request body: listing, counting and exporting records, deleting them, associating
and dissociating related records, running an action, its form hooks and its approval, and charts.
Users allowed to run a segment keep every feature on it. Live query segments and SQL charts run
in a transaction that is always rolled back, read-only on Postgres.

Definition of Done

General

  • Write an explicit title for the Pull Request, following Conventional Commits specification
  • Test manually the implemented changes
  • Validate the code quality (indentation, syntax, style, simplicity, readability)

Security

  • Consider the security impact of the changes made

🤖 Generated with Claude Code

Note

Check live-query segment permission on every route that uses one

Adds AuthorizationService.assertCanExecuteLiveQuerySegment, which parses the live-query segment from the request and throws Forbidden when the user may not execute it for the collection. Removes the implicit segment check from assertCanOnCollection, and every read and modification route now calls the explicit assertion before building filters or running queries.

  • ActionRoute.getRecordSelection skips the segment check and filter when a non-global action has a positive explicit ID selection.
  • Adds tests covering denied and allowed segments for listing, counting, CSV, charts, actions, deletes, associations, and dissociation, including the explicit-selection bypass.

Changes since #2046 opened

  • Modified SequelizeDataSource.executeNativeQuery in datasource-sequelize to run native queries inside a transaction that is always rolled back. [b68cd93]
  • Refactored the datasource-sequelize unit tests for SequelizeDataSource.executeNativeQuery and added transaction and rollback test cases. [b68cd93]
  • Added an integration test suite for SequelizeDataSource.executeNativeQuery in datasource-sequelize across multiple database dialects. [b68cd93]

Macroscope summarized 409bce0.

@linear-code

linear-code Bot commented Oct 27, 2026

Copy link
Copy Markdown

PRD-1759

@qltysh

qltysh Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Qlty


Coverage Impact

⬆️ Merging this pull request will increase total coverage on main by 0.1%.

Modified Files with Diff Coverage (13)

RatingFile% DiffUncovered Line #s
Coverage rating: A Coverage rating: A
packages/agent/src/routes/modification/associate-related.ts100.0%
Coverage rating: A Coverage rating: A
...ges/agent/src/routes/modification/dissociate-delete-related.ts100.0%
Coverage rating: A Coverage rating: A
packages/agent/src/routes/access/count-related.ts100.0%
Coverage rating: A Coverage rating: A
packages/datasource-sequelize/src/datasource.ts100.0%
Coverage rating: A Coverage rating: A
packages/agent/src/services/authorization/authorization.ts100.0%
Coverage rating: A Coverage rating: A
packages/agent/src/routes/access/list-related.ts100.0%
Coverage rating: A Coverage rating: A
packages/agent/src/routes/modification/action/action.ts100.0%
Coverage rating: A Coverage rating: A
packages/agent/src/routes/modification/delete.ts100.0%
Coverage rating: A Coverage rating: A
packages/agent/src/routes/access/csv.ts100.0%
Coverage rating: A Coverage rating: A
packages/agent/src/routes/access/chart.ts100.0%
Coverage rating: A Coverage rating: A
packages/agent/src/routes/access/csv-related.ts100.0%
Coverage rating: A Coverage rating: A
packages/agent/src/routes/access/count.ts100.0%
Coverage rating: A Coverage rating: A
packages/agent/src/routes/access/list.ts100.0%
Total100.0%
🚦 See full report on Qlty Cloud »

🛟 Help
  • Diff Coverage: Coverage for added or modified lines of code (excludes deleted files). Learn more.

  • Total Coverage: Coverage for the whole repository, calculated as the sum of all File Coverage. Learn more.

  • File Coverage: Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)

    • Indirect Changes: Changes to File Coverage for files that were not modified in this PR. Learn more.

…segment is used

Each route that builds its filter from a live query segment checks that
the user may run that segment, on the collection whose filter carries it,
whether the segment comes from the query string or the request body:
listing, counting and exporting records, deleting them, associating and
dissociating related records, running an action, its form hooks and its
approval, and charts.

Users allowed to run a segment keep every feature on it.

Refs: PRD-1759

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Scra3
Scra3 force-pushed the fix/prd-1759-check-segment-query-permission branch from 29ad71f to a70ec69 Compare October 27, 2026 14:14
@Scra3
Scra3 marked this pull request as ready for review October 27, 2026 14:41
alban bertolini and others added 2 commits October 27, 2026 16:13
…utes

Refs: PRD-1759

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…elpers using it

Refs: PRD-1759

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@Scra3 Scra3 left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Spec (PRD-1759): conforms to the ticket's fix. 1 unvalidated decision: not final.

Explicit ids already pin the records, so the selection's live query segment
is neither checked nor run, as forest-express and forest-rails do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@qltysh

qltysh Bot commented Oct 28, 2026

Copy link
Copy Markdown

1 new issue

Tool Category Rule Count
qlty Structure Function with high complexity (count = 12): getRecordSelection 1

@ShohanRahman

Copy link
Copy Markdown
Contributor

Design note, not blocking.

The behaviour is consistent with agent-ruby: 403 ForbiddenError from canExecuteSegmentQuery, segment read from the query string or all_records_subset_query, and 422 when the connection name is missing. Nothing to change for alignment.

On structure, I'd have put the check where the filter is built rather than next to each call. agent-ruby runs can_execute_query_segment? inside build_context_filter (routes/query_handler.rb), so any route that builds a filter from the request checks the segment without having to remember to. Here, assertCanExecuteLiveQuerySegment is called by hand in 11 routes right before ContextFilterFactory.build/buildPaginated. A route added later that calls the factory would pick up the segment from the request without the check.

A follow-up could make the factory async and pass it the authorization service and the collection name (or add a buildChecked next to it), so that building a filter that carries a segment always goes through the permission. The explicit-id action case would stay a parameter (liveQuerySegment: undefined). It would also remove most of the per-route mocks in the tests. Better as its own PR than in this one.

@Scra3

Scra3 commented Oct 30, 2026

Copy link
Copy Markdown
Member Author

Heads-up for whoever closes PRD-1759: once this PR lands, verifySQLQuery becomes the only guard on every path that runs a live query segment. A follow-up residual on that check (dialect-specific) is documented in the Linear ticket, so this PR shouldn't be treated as the complete fix for the ticket on its own. Details are in Linear only.

…always rolled back

Live query segments and SQL charts only read. executeNativeQuery now runs
them in a transaction it always rolls back, read-only on Postgres, so no
statement they run is ever committed, whatever the dialect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
{
const { instance } = this.nativeQueryConnections[connectionName] as NativeQueryConnection;
// Live queries only read: whatever their text runs, it is rolled back, never committed.
const transaction = await instance.transaction();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Critical src/datasource.ts:63

With SQLite storage: ':memory:', a concurrent collection.create can return success while its write is silently undone by this rollback. Sequelize shares the in-memory connection between transactional and ordinary queries, so the create joins this transaction; isolate this execution or coordinate access to the shared connection before rolling it back.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/datasource-sequelize/src/datasource.ts around line 63:

With SQLite `storage: ':memory:'`, a concurrent `collection.create` can return success while its write is silently undone by this rollback. Sequelize shares the in-memory connection between transactional and ordinary queries, so the create joins this transaction; isolate this execution or coordinate access to the shared connection before rolling it back.

This branch has not been deployed

No deployments
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