Repository navigation
test(agent): lock the read guard on action record selections [PRD-1026] - #424
Conversation
PRD-1667 (#414) routed get_record_selection through build_context_filter, so a filter on a collection the caller cannot read is refused before can_smart_action? counts it and before the form hooks see it. The spec stubbed that guard away, so dropping the helper would reopen the conditional-approval oracle with no failing test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Coverage Impact This PR will not change total coverage. 🚦 See full report on Qlty Cloud »🛟 Help
|
… [PRD-1026] - Filter on a related collection through a holder relation in the fixture - Make refusals depend on the filtered field instead of raising for any call - Compare guarded and applied trees by leaf identity, whatever their shape - Assert form and execute calls on the published collection the route uses - Cover caller filters sent outside the subset query - Share the select-all setup through a parent context and a let Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-1026] - Assert an Or caller filter reaches the count as the guarded branch - Cover caller filters sent in params[:filter] as well as params[:filters] - Share the published collection and the guarded tree capture Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hercemer42
left a comment
There was a problem hiding this comment.
Spec (PRD-1026): conforms.
Forest Validator · Verify fixes: ✅ 1 closed
1 earlier step
|
- Check the collection, action name and subset query handed to the count - Check the guard gets no search besides the condition tree - Check caller, action, data, filter and meta handed to the form hooks - Compare the published collection by identity to keep failures readable Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Check id, email, rendering id and timezone of the caller given to get_form - Check the meta handed to get_form on the searched and segmented list Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-1026] - Check the full condition tree counted and handed to the form hooks - Check search, segment, sort and page on both filters - Check the request args handed to the count by identity - Pin each guard, count and form call to a single call Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hercemer42
left a comment
There was a problem hiding this comment.
Approved after /verify-fixes run 424-20261105143628-hercemer42-verify: 1 findings of 424-20261103112700-hercemer42 closed or accepted, none reopened.

Closes PRD-1026. Same situation as #423 (PRD-1025).
Context
PRD-1026 reports that the action routes apply the caller's
filterswithout checking that the caller can read the collections those filters reach. The serious case is executing an action that has a conditional trigger or approval rule.SmartActionCheckercounts the records that match the filter, and the request then ends three different ways: the action runs, it needs approval, or it is forbidden. Which one depends on the caller's filter, so the caller learns one bit per request about a column they cannot read, and the action never runs.That is no longer true on
main. #414 (PRD-1667) routedget_record_selectionthroughbuild_context_filter, which callsassert_can_read_query_fields. Both entry points build the selection before anything uses it:handle_requestbuilds it beforecan_smart_action?, so a filter on an unreadable column gets a 403 before any count;handle_hook_requestbuilds it beforeget_form, so the filter never reaches the customer's form hooks.The guard covers
searchtoo, which the ticket assumed could not reach these routes. The out-of-date comment the ticket mentioned ("Match user filter + search + scope? + segment") is already gone.The spec stubbed the guard to
nil. The only test that checked it was called was on the hook route, withsearchalone. Nothing testedfilters, or that a refusal stops the request before the count.Not covered here: the related-data parent
get_record_selectionalso narrows the selection with a parent the caller names (parent_collection_name,parent_collection_id,parent_association_name), with no read or scope check on that parent collection. That feeds the same count, so it is the same one-bit oracle by another input. It is not about the caller'sfilters, so it is tracked separately in PRD-1929 and stays open after this PR.Changes
Specs only, no application code changed.
bookgets a many-to-oneholderrelation to a newholdercollection with anational_idcolumn, so the caller's filter targets a related collection, as in the ticket.let.handle_request, with anAndfilter onholder:national_idandtitle:can_smart_action?counts with (compared by object identity, whatever the tree's shape);can_smart_action?,get_formandexecuteare not called.handle_hook_request:get_formreceives carries the very conditions the guard checked;get_formis not called.Orfilter: the guardedOrbranch reaches the count as is, so a route that kept the conditions but rebuilt the branch as anAndwould fail.params[:filters]orparams[:filter], outsideall_records_subset_query, is refused the same way.action_name:and subset query handed tocan_smart_action?, that the guard gets no search besides the condition tree, and the caller, action name, data, filter attributes and meta handed toget_form.The refusal stub only raises when the checked tree contains
holder:national_id, so the assertions depend on the field being filtered rather than on a fixed message.get_formandexecuteare stubbed and asserted on the published collection, which is the one the route calls. The real guard's handling of relation paths is unit-tested inrelated_read_permissions_spec.rb.Four checks, all reverted after running:
query_handler.rbpasscondition_tree: nilto the guard fails all 7 new examples;Orcaller tree as anAndof the same conditions fails only theOrexample;@route_collectioninstead ofcontext.collectiontocan_smart_action?fails 2 examples;action_name: 'bar'fails 3.The published collection is compared with
equal?and a short message: on a mismatch,eq/beprint both decorated collections, and that output never finishes.Definition of Done
General
Security
🤖 Generated with Claude Code
Note
Add specs locking the read-field guard behavior on action record selections
holder:national_idOrconditions keep the guarded branchparams[:filters]orparams[:filter]Changes since #424 opened
guarded_treehelper expectation onpermissions.assert_can_read_query_fieldsin the Actions spec. [31287b8]counted_filterhelper and switched the 'checks the very condition tree the authorization then counts with' and 'counts with the guarded branch as is' examples to use it. [31287b8]get_formcall expectations in the 'hands the form hooks only a filter the guard checked' example. [31287b8]bearer_callermatcher and replaced the generic caller expectation with it in theget_formexpectations [4b1b6a5]metaargument in the 'when the list is searched and segmented' example [4b1b6a5]ForestAdminAgent::Routes::Action::Actionsspec. [5858c5f]ForestAdminAgent::Routes::Action::Actionsspec. [5858c5f]Macroscope summarized 1c115e4.