Skip to content

security(cr): validate program access on assign-program detail#338

Draft
gonzalesedwin1123 wants to merge 5 commits into
19.0from
security-cr-assign-program-access-bypass
Draft

security(cr): validate program access on assign-program detail#338
gonzalesedwin1123 wants to merge 5 commits into
19.0from
security-cr-assign-program-access-bypass

Conversation

@gonzalesedwin1123

@gonzalesedwin1123 gonzalesedwin1123 commented Jul 23, 2026

Copy link
Copy Markdown
Member

Summary

The spp_cr_type_assign_program apply strategy validated a selected program's
existence, state, target type, and uniqueness — but not the user's access
to the program or its company. The detail's program_id domain only
constrains the UI, so an authenticated CR user with detail write access could
submit a raw ORM/RPC write({'program_id': <arbitrary id>}). On apply, core CR
code runs the strategy on self.sudo() (spp.change.request._do_apply), so
program record rules and multi-company scope were bypassed during validation and
membership creation — assigning the registrant to a hidden/cross-company
program — and preview generation leaked that program's display name through the
same sudo path.

Fix

Add @api.constrains('program_id') to spp.cr.detail.assign_program that, in
the writing user's own context, rejects a program they cannot access:

  • visibility — a search() must return the program (enforces any record
    rule on spp.program, and rejects a stale/deleted id);
  • multi-company scope — the program's company_id must be within
    self.env.companies. This explicit check is load-bearing, not just
    defense-in-depth
    : verified by experiment that the global multi-company
    ir.rule on spp.program is not applied to the search in this
    write/constraint context, so search alone would let a cross-company program
    through.

This guards the only sender-reachable input path to program_id; the sudo
apply/preview then only ever consume an already-validated, accessible program.
The apply strategy is annotated to make explicit that access is enforced at
detail-write time, so any future code that sets program_id under sudo must
perform its own access check.

Design decision (confirmed with owner)

Constraint-only (no extra check inside the sudo validate()): the constraint
blocks storing an inaccessible program_id at the source, and the module is
unreleased so no pre-existing poisoned records exist.

Testing

  • ./spp t spp_cr_type_assign_program23 passed, 0 failed, 0 errors (19
    existing + 4 new), run as a non-admin group_cr_user.
  • New tests: a company-A CR user is rejected when writing, and when creating, a
    detail pointing at a company-B program; is allowed to select a company-A
    program; and is allowed to select a company-shared (company_id = False)
    program.

Staff review

Two independent adversarial staff reviews were run. Verdict: ship-with-nits, no
open security hole. Findings addressed in-branch:

  • Corrected the constraint's rationale (the company check is load-bearing —
    proven by removing it and watching the reject tests fail).
  • Documented in the apply strategy that access is enforced at write time.
  • Made the tests robust to in-flight security(cr): add record rules to CR detail models (ownership + area) #261's detail-ownership rule (use the CR
    user's own partner as registrant) and added the shared-program allow case.

Consciously left (low, non-security): an archived program is reported as "no
access" rather than "archived" (apply re-validates state regardless); and a
user lacking any programs-read group gets AccessError rather than the friendly
ValidationError (pre-existing — the feature already requires a programs group).

Notes for reviewers

  • Related in-flight PR security(cr): add record rules to CR detail models (ownership + area) #261 (cr-detail-record-rules) also touches this
    module — it adds parent-CR-ownership/area record rules to the detail
    (which detail records you may write). This PR is complementary: it governs
    which program you may target on a detail you legitimately own. Both bump
    the manifest/HISTORY/README, so a version/changelog collision is expected;
    this PR uses 19.0.1.0.3 (higher than both the 19.0 base 19.0.1.0.1 and
    security(cr): add record rules to CR detail models (ownership + area) #261's bump), so whichever merges second needs a trivial re-merge.
  • No migration: spp_cr_type_assign_program is not in any released tag.
  • README generated files are applied from CI's pinned oca-gen verbatim.

Draft: awaiting human security review before undraft/merge.

Merge order (CR security cluster)

Per the cross-PR interaction analysis (2026-07-24, internal/plans/security-prs-interaction-analysis.md), hard order: #264#261#338 (this PR) → #343.

The spp.cr.detail.assign_program program_id domain only constrains the UI, so
a CR user could RPC-write an arbitrary program id. The apply strategy runs on
self.sudo(), so program record rules and multi-company scope were bypassed at
validation, membership creation, and preview - allowing assignment to a
hidden/cross-company program and leaking its name.

Add @api.constrains('program_id') on the detail that, in the writing user's own
context, rejects a program the user cannot see (record rules) or that is outside
their company scope (explicit env.companies check, since the global company
ir.rule is not reliably applied to searches in the write context).

Bump 19.0.1.0.1 -> 19.0.1.0.3 (dodging #261's 19.0.1.0.2 on the same module) +
HISTORY. No migration: module is not in any released tag.
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@codecov

codecov Bot commented Jul 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.00000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 68.10%. Comparing base (1caf794) to head (fbf04f6).
⚠️ Report is 3 commits behind head on 19.0.

Files with missing lines Patch % Lines
...p_cr_type_assign_program/details/assign_program.py 90.00% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             19.0     #338      +/-   ##
==========================================
- Coverage   74.28%   68.10%   -6.18%     
==========================================
  Files         372      101     -271     
  Lines       25385     8422   -16963     
==========================================
- Hits        18857     5736   -13121     
+ Misses       6528     2686    -3842     
Flag Coverage Δ
spp_analytics ?
spp_api_v2_cycles ?
spp_api_v2_entitlements ?
spp_api_v2_gis ?
spp_api_v2_programs ?
spp_api_v2_simulation ?
spp_audit_programs ?
spp_base_common 91.07% <ø> (+0.80%) ⬆️
spp_case_demo ?
spp_case_entitlements ?
spp_case_programs ?
spp_cr_type_assign_program 91.81% <90.00%> (+0.64%) ⬆️
spp_dci_compliance ?
spp_dci_demo ?
spp_dci_server_social ?
spp_demo ?
spp_demo_phl_luzon ?
spp_drims ?
spp_drims_sl ?
spp_drims_sl_demo ?
spp_farmer_registry_demo ?
spp_gis_report ?
spp_grm_demo ?
spp_indicator ?
spp_indicator_studio ?
spp_metric_service ?
spp_mis_demo_v2 ?
spp_programs 65.27% <ø> (+<0.01%) ⬆️
spp_registry 86.94% <ø> (+0.10%) ⬆️
spp_security 69.56% <ø> (+2.89%) ⬆️
spp_simulation ?

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

Files with missing lines Coverage Δ
...r_type_assign_program/strategies/assign_program.py 93.75% <ø> (ø)
...p_cr_type_assign_program/details/assign_program.py 86.84% <90.00%> (+0.63%) ⬆️

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

…angelog

Applies CI's pinned oca-gen output verbatim for the new HISTORY fragment.
The keep-a-changelog '## <version> (date)' / '### Fixed' fragment made the
oca-gen README generator crash (docutils 'Inconsistent title style: skip from
level 2 to 4'). Switch to OCA flat '### <version>' entries (matching the
module's DESCRIPTION heading ladder). README.rst reverted to base so CI's
pinned generator regenerates it cleanly.
…TORY

Applies CI's pinned oca-gen output verbatim for the OCA-style changelog.
- Correct the constraint docstring: the explicit company check is load-bearing,
  not defense-in-depth. Verified by experiment that the global multi-company
  ir.rule on spp.program is NOT applied to the search in this write/constraint
  context, so search-only would let a cross-company program through; drop the
  inaccurate 'area/registrant record rules' claim (spp.program has only the
  company rule).
- Document in the apply strategy that program access is enforced at detail
  write time, so any future sudo writer of program_id must re-check.
- Make the tests robust to in-flight PR #261's detail-ownership rule by using
  the cr_user's own partner as the CR registrant (matches #261's registrant
  branch); add a shared-program (company_id=False) allow test.

Tests: 23 pass (was 22), run as a non-admin group_cr_user.
@gonzalesedwin1123

Copy link
Copy Markdown
Member Author

Follow-up filed: #347 — this PR guarantees the target program is accessible, but program_id remains repointable post-submit (approval-fidelity gap); fix is an assign-program override of #264's _protected_content_fields.

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