Skip to content

security(hazard): gate stored registrant impact indicators with field-level groups (fix-up for #262) - #514

Merged
gonzalesedwin1123 merged 4 commits into
19.0-staging-sec-batch3from
security-hazard-impact-acl-fixup
Sep 10, 2026
Merged

security(hazard): gate stored registrant impact indicators with field-level groups (fix-up for #262)#514
gonzalesedwin1123 merged 4 commits into
19.0-staging-sec-batch3from
security-hazard-impact-acl-fixup

Conversation

@gonzalesedwin1123

@gonzalesedwin1123 gonzalesedwin1123 commented Sep 10, 2026

Copy link
Copy Markdown
Member

Fix-up for #262 on the batch-3 staging branch, from the merge-turn adversarial review (record: internal plans batch3-merge-turn-review.md). Targets 19.0-staging-sec-batch3; lands as an additional reviewed squash commit before umbrella #513 merges.

Finding (HIGH, confirmed)

res.partner.hazard_impact_count and has_active_impact are stored columns derived from spp.hazard.impact. #262 removed every internal user's read on the impact model and gated the registrant list/search views, but a stored field without field-level groups= is still readable through the ORM regardless of views. Any base.group_user could run

search_read('res.partner', [('has_active_impact','=',True)], ['name','hazard_impact_count'])

and get a per-registrant list of disaster victims with impact counts — strictly worse than the incident-level aggregate #262 did gate.

Change

  • Field-level groups="spp_hazard.group_hazard_read,spp_registry.group_registry_viewer,spp_security.group_spp_admin" on both fields (same set as the impact read ACL and security(hazard): restrict impact records to hazard/registry roles #262's view gating).
  • Every form/list/search element referencing the fields already sits inside security(hazard): restrict impact records to hazard/registry roles #262's gated stat button / Emergency Response page / gated columns, so a restricted user's arch loses them together. Repo-wide grep: no reader of either field outside spp_hazard.
  • HISTORY bullet added under the still-unreleased 19.0.2.1.1; no extra version bump. README/index.html regeneration is left to CI's pinned oca-gen-addon-readme (its diff will be applied as a follow-up commit if pre-commit reports one).

Second finding refuted

The review also flagged that the two computes query spp.hazard.impact in the calling user's env and run on every partner create, so users without impact read would hit AccessError creating contacts. Not so: stored computes run with compute_sudo=True by default. test_contact_creator_without_impact_read_can_create_partner creates a partner as a Contact-Creation-only user, flushes in that user's env, and passes on the unfixed staging code. Kept as a guard.

Tests

spp_hazard locally on the current image: 83 passed, 0 failed (80 + 3 new). test_plain_internal_user_cannot_read_registrant_impact_fields fails without the groups=; test_hazard_viewer_can_read_registrant_impact_fields pins the privileged path.

Round 2 (delta review + Edwin's decision)

  • Delta review (SHIP-WITH-NITS) confirmed from Odoo 19 source that field-level groups= is enforced on search domains, order=, read_group, export and relational traversal, so no presence oracle remains. Its should-fix taken: hazard_impact_ids (the O2M) now carries the same groups=, so a bare read() of a partner by a user without impact read no longer fails on it. Tests extended to pin domain / order / read_group / O2M / fields_get / list-arch paths.
  • spp_hazard_programs gate (review finding 3, accepted by Edwin): the emergency aggregate affected_registrant_count stays visible to every program user, but the list of impacted registrants is the identity linkage the impact ACL protects. action_view_affected_registrants now runs check_access("read") on spp.hazard.impact (buttons are RPC-callable), its stat button is gated with the same groups=, and get_emergency_eligible_registrants() is renamed _get_emergency_eligible_registrants() so it is no longer reachable over RPC. No overrides exist in this repo, customer repos or legacy modules. 3 tests added; spp_hazard_programs 28/28 locally. HISTORY 19.0.2.0.1 amended (unreleased in this batch); README regen via CI's diff.

…level groups

Merge-turn review of #262 on the batch-3 staging branch found that
res.partner.hazard_impact_count and has_active_impact are stored columns
derived from the sensitive impact table, and #262 gated only the list and
search views. Any internal user could still search_read / read_group them
over RPC and enumerate which registrants are disaster victims, per person.

Apply the same field-level groups= as the impact model's read ACL to both
fields. All form/list/search elements that reference them already sit inside
#262's gated containers, and no other module reads them.

The review also claimed that removing base.group_user read on impacts would
break partner creation for users without impact read, because the computes
query spp.hazard.impact. That does not hold: stored computes run as
superuser (compute_sudo defaults to True for stored fields). A test creating
a partner as a Contact Creation-only user, flushing in that user's env, pins
the behaviour.

Tests: spp_hazard 83/83 locally (3 new; the RPC-read test is red without the
groups=). HISTORY amended under the unreleased 19.0.2.1.1; no extra bump.
@codecov

codecov Bot commented Sep 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 74.89%. Comparing base (e428de5) to head (9f17740).

Additional details and impacted files

Impacted file tree graph

@@                     Coverage Diff                     @@
##           19.0-staging-sec-batch3     #514      +/-   ##
===========================================================
+ Coverage                    74.52%   74.89%   +0.36%     
===========================================================
  Files                          624      639      +15     
  Lines                        43954    44599     +645     
===========================================================
+ Hits                         32757    33401     +644     
- Misses                       11197    11198       +1     
Flag Coverage Δ
spp_api_v2_change_request 73.37% <ø> (ø)
spp_api_v2_cycles 71.03% <ø> (ø)
spp_api_v2_entitlements 70.23% <ø> (ø)
spp_api_v2_gis 74.60% <ø> (ø)
spp_api_v2_programs 92.22% <ø> (ø)
spp_api_v2_simulation 71.19% <ø> (ø)
spp_base_common 91.07% <ø> (ø)
spp_case_entitlements 100.00% <ø> (?)
spp_case_programs 100.00% <ø> (?)
spp_cel_load_testing 98.11% <ø> (?)
spp_change_request_v2 78.76% <ø> (+0.92%) ⬆️
spp_cr_type_assign_program 92.50% <ø> (+0.42%) ⬆️
spp_hazard 99.20% <ø> (ø)
spp_hazard_programs 98.61% <100.00%> (+0.01%) ⬆️
spp_programs 67.58% <ø> (ø)
spp_registry 88.94% <ø> (ø)
spp_security 69.56% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
spp_hazard/models/registrant.py 100.00% <ø> (ø)
spp_hazard_programs/models/program.py 98.21% <100.00%> (+0.03%) ⬆️

... and 23 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

… paths; regenerate README

Delta review of the fix-up: res.partner.hazard_impact_ids searches
spp.hazard.impact in the reading user's env, so a bare read() of a partner by
a user without impact read would fail on that field. Give the O2M the same
field-level groups= as the two stored indicators.

Extend the RPC test to cover the headline attack surface directly: search
domains and order= on the gated columns, read_group over them, the O2M in a
field list, fields_get() hiding all three, and the list arch stripping the
columns. README.rst / index.html regenerated from CI's pinned oca-gen output.

spp_hazard ACL suite 12/12 locally.
…gistrant list

#262 kept the emergency aggregate (affected_registrant_count) readable by
every program user, which is fine: a number. But the 'Affected' stat button
and the public get_emergency_eligible_registrants() returned the list of
impacted registrants to any spp.program reader with no hazard role, over the
UI and over RPC. That list is the identity linkage the impact ACL protects.

- action_view_affected_registrants: check_access('read') on
  spp.hazard.impact before building the action (buttons are RPC-callable).
- The stat button carries the same field-level groups= as the impact ACL.
- get_emergency_eligible_registrants -> _get_emergency_eligible_registrants:
  private, so not reachable via call_kw; Python callers and overrides keep
  working (no overrides exist in this repo, customers or legacy modules).

Tests: 3 added in test_program_user_access.py (button stripped + action
refused for a program user without impact read; method not public; hazard
viewer keeps both); spp_hazard_programs 28/28 locally. HISTORY 19.0.2.0.1
amended (unreleased); DESCRIPTION updated; README regen left to CI.
@gonzalesedwin1123
gonzalesedwin1123 merged commit d482b38 into 19.0-staging-sec-batch3 Sep 10, 2026
35 checks passed
@gonzalesedwin1123
gonzalesedwin1123 deleted the security-hazard-impact-acl-fixup branch September 10, 2026 06: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.

1 participant