Skip to content

security(hazard): restrict impact records to hazard/registry roles - #262

Draft
gonzalesedwin1123 wants to merge 9 commits into
19.0from
security-hazard-impact-acl
Draft

security(hazard): restrict impact records to hazard/registry roles#262
gonzalesedwin1123 wants to merge 9 commits into
19.0from
security-hazard-impact-acl

Conversation

@gonzalesedwin1123

@gonzalesedwin1123 gonzalesedwin1123 commented Jul 1, 2026

Copy link
Copy Markdown
Member

Do not merge yet — held for human review.

Summary

spp_hazard's ACL granted base.group_user (every internal user) read on spp.hazard.impact, which links named registrants to incidents with damage level, dates, verification status, notes and verifier. The menu is gated to hazard groups, but the ACL left this sensitive data open to direct RPC/search_read by any internal user. Dedicated hazard groups (read/viewer/officer/manager) and registry_viewer already grant the intended read, so the base.group_user grant on impact was redundant and over-broad.

Fix (surgical — impact only)

  • ACL: remove the base.group_user read row for spp.hazard.impact only. The four non-PII reference/operational models (category, incident, incident.area, impact.type) keep broad internal read — sibling modules (notably spp_drims, which reads incidents heavily) rely on that, and none of them read impact.
  • spp_hazard_programs: read spp.hazard.impact via sudo() in the two emergency-eligibility computes, so a program user without a hazard group isn't blocked (only aggregate counts / eligible registrants surface, never impact rows).
  • spp_hazard_programs (perf): those two sudo() queries now aggregate in SQL via _read_group (registrant_id:count_distinct for the count; groupby=["registrant_id"] for the eligible-registrants recordset) instead of search() + mapped("registrant_id"), so no impact rows are loaded into memory — the sudo scope makes them scan all qualifying impacts, not just an ACL-visible subset (review feedback). The eligible-registrants recordset is returned in the caller's env, not the sudo one.
  • Views: gate every element that renders impact rows to users who actually have impact read (group_hazard_read / registry_viewer / group_spp_admin) — the registrant form's stat button, Emergency Response page (with the impact O2M), list columns and search filters; and the incident form's Impacts page + "Affected" stat button. This prevents AccessError for non-hazard users who reach these forms (e.g. via a CR's registrant link, or DRIMS' incident dashboard).

Why not remove all 5 / add a record rule?

Removing base.group_user from all 5 models broke spp_drims (incident reads) and spp_hazard_programs in an earlier revision — caught in review. Impact is the only model with registrant PII; the rest are non-sensitive and depended upon. No record rule is needed: hazard viewers are meant to see all hazard rows (no per-record ownership).

Tests

  • base.group_user is denied read on impact but retains the 4 non-sensitive models.
  • Hazard viewer and a registry officer (no hazard group) retain full impact access (live O2M read).
  • A program user without a hazard group can still compute emergency eligibility (exercises the sudo path live).
  • The incident form's impact_ids O2M is stripped from the arch for a non-hazard user and present for a hazard user.
  • Green: spp_hazard 88/88 · spp_hazard_programs 25/25 · spp_drims 250/250. Lint/semgrep clean.

Reviewed

Three adversarial staff-review passes. The first two each caught a real regression (all-5 removal breaking DRIMS/hazard_programs; then the ungated incident-form O2M) — both fixed. Final pass: APPROVE, no blockers; an exhaustive repo-wide sweep confirmed no remaining path where a non-hazard user can read spp.hazard.impact.

Merge order (vs #274 hazard-cap reland)

Per the cross-PR interaction analysis (2026-07-24, internal/plans/security-prs-interaction-analysis.md): this PR merges before #274 (which is held per the PR76 re-land plan anyway).

spp_hazard's ACL granted base.group_user (every internal user) read on
spp.hazard.impact, which links named registrants to incidents with damage
level, dates, verification status, notes and verifier. The menu is gated to
hazard groups, but the ACL left this sensitive data open to direct RPC/
search_read by any internal user. Dedicated hazard groups and registry_viewer
already grant the intended read, so the base.group_user grant on impact was
redundant and over-broad.

Fix (surgical — impact only):
- Remove the base.group_user read row for spp.hazard.impact. The four non-PII
  reference/operational models (category, incident, incident.area, impact.type)
  keep broad internal read, which sibling modules (spp_drims) rely on.
- spp_hazard_programs: read spp.hazard.impact via sudo in the emergency-program
  eligibility computes, so a program user without a hazard group is not blocked
  (only aggregate counts / eligible registrants are surfaced, not impact rows).
- Registrant form: gate the hazard-impact stat button, Emergency Response page,
  list columns and search filters to users with impact read (hazard groups /
  registry_viewer / admin), so non-hazard users reaching the form (e.g. via a
  CR's registrant link) don't render impact data.

Tests: base.group_user is denied read on impact but retains the 4 non-sensitive
models; hazard viewer and registry officer retain full impact access; a program
user without a hazard group can still compute emergency eligibility.
spp_hazard 86/86, spp_hazard_programs 25/25, spp_drims 250/250.

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request restricts broad read access to the sensitive spp.hazard.impact model by removing its ACL for base.group_user and adding explicit group restrictions to several views. To prevent access errors for non-hazard program users, the emergency eligibility computations now query the impact model using sudo(). The review feedback highlights performance bottlenecks in these computations, recommending the use of _read_group instead of search() to avoid loading large numbers of impact records into memory.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread spp_hazard_programs/models/program.py Outdated
Comment thread spp_hazard_programs/models/program.py Outdated
@codecov

codecov Bot commented Jul 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 75.62%. Comparing base (925bd4b) to head (8d07455).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             19.0     #262      +/-   ##
==========================================
- Coverage   76.18%   75.62%   -0.57%     
==========================================
  Files         661     1087     +426     
  Lines       44110    64707   +20597     
==========================================
+ Hits        33605    48934   +15329     
- Misses      10505    15773    +5268     
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_attendance ?
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.63% <ø> (ø)
spp_consent ?
spp_cr_type_assign_program 92.50% <ø> (ø)
spp_data_classification ?
spp_dci_client_compliance ?
spp_dci_client_crvs ?
spp_grm_cel ?
spp_hazard 99.20% <ø> (ø)
spp_hazard_programs 98.59% <100.00%> (?)
spp_key_management ?
spp_pii_encryption ?
spp_programs 66.97% <ø> (ø)
spp_registry 87.79% <ø> (ø)
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/hazard_incident.py 98.05% <ø> (ø)
spp_hazard_programs/models/program.py 98.18% <100.00%> (ø)

... and 638 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.

Record the hazard impact ACL restriction (spp_hazard 19.0.2.0.3) and the
sudo eligibility-compute fix (spp_hazard_programs 19.0.2.0.1) in versions and
HISTORY changelogs (regenerated READMEs).
@gonzalesedwin1123
gonzalesedwin1123 force-pushed the security-hazard-impact-acl branch from 155f201 to 5e9de01 Compare July 2, 2026 14:38
The earlier README regeneration was run in a local env whose RST renderer
produced wider table columns than CI's pinned oca-gen environment, causing the
'Generate addons README files from fragments' pre-commit hook to fail on CI.
Restore the column widths CI's generator produces.
Bring the branch up to date with 19.0 (30 commits, incl. the geofence
partial-revert #271). Conflict in spp_hazard/views/hazard_incident_views.xml
resolved by adopting 19.0's Impacts-page rework while preserving this PR's
security `groups=` gate on the Impacts page and the 'Affected' stat button.
Auto-merged manifest (kept our 2.0.3 security bump) and tests/__init__.py
(kept our test_acl_group_user import, took 19.0's removal of test_alert_ingestion).
@gonzalesedwin1123
gonzalesedwin1123 marked this pull request as draft July 20, 2026 01:19
… SQL

Replace search()+mapped('registrant_id') with _read_group so impact
rows are never loaded into memory (review feedback on the sudo'd
queries, which now scan all impacts rather than the ACL-visible
subset). The eligible-registrants recordset is returned in the
caller's env instead of the sudo one.
spp.hazard.incident stays broadly readable (sibling modules read
incidents), but affected_registrant_count is derived from the sensitive
spp.hazard.impact table via raw ACL-bypassing SQL. Add field-level
groups= so a plain internal user cannot read the affected-registrant
aggregate over RPC, and gate the incident list column. Adds regression
tests.
Assign the field read to a throwaway so the assertRaises still exercises Field.__get__ without tripping pylint W0104. Regenerate spp_hazard README from the updated HISTORY fragment.
19.0's #416 took spp_hazard 19.0.2.1.0 (incident status lifecycle) past this
PR's 2.0.3: re-bump to 19.0.2.1.1 and ladder the HISTORY. The code composed
cleanly — #416's status-workflow additions reference no impact data, the
affected_registrant_count field gate and both gated view references survive,
and the Impacts page carries both #416's closed-incident readonly and this
PR's groups=. Merged suites green: spp_hazard 80/0, spp_hazard_programs 25/0.
README regenerated via the pinned oca-gen.
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