Skip to content

fix(spp_programs): close membership ACL bypass and approval/payment bugs#241

Open
kneckinator wants to merge 1 commit into
19.0from
fix/security-scan-findings
Open

fix(spp_programs): close membership ACL bypass and approval/payment bugs#241
kneckinator wants to merge 1 commit into
19.0from
fix/security-scan-findings

Conversation

@kneckinator

Copy link
Copy Markdown
Contributor

Summary

Fixes four issues surfaced by a security scan of recent performance refactors in spp_programs. Each fix has a regression test.

Area Problem Fix
Membership ACL bypass (high) bulk_create_memberships is a public, RPC-callable @api.model method whose skip_duplicates=True path runs raw INSERT ... ON CONFLICT and whose program-membership ORM path runs through sudo() — both bypassing ACL checks, letting a low-privileged user create memberships (incl. enrolled) they couldn't create via the ORM. Gate the helper with self.check_access("create") at entry, so all paths enforce model-level create rights.
NULL-state approval (medium) _compute_all_entitlements_approved used state != 'approved'; in SQL NULL != 'approved' is UNKNOWN, so a NULL-state (unapproved) entitlement was excluded and the cycle was reported as fully approved. Use state IS DISTINCT FROM 'approved'.
Fund balance per program (low) approve_entitlements fetched the balance once via entitlements[0] and reused it for the whole batch — a mixed-program batch was checked against the wrong program's funds, and an empty recordset crashed on entitlements[0]. Track balance/running-total per program_id (one check per program, optimization preserved) and return cleanly for an empty recordset. Applied to both DefaultCashEntitlementManager and SppCashEntitlementManager.
Empty payment batches (low) _prepare_payments only wrote batch_id; since spp.payment.batch.payment_ids is an independent Many2many (not the inverse of batch_id), generated batches displayed/iterated zero payments. Also populate curr_batch.payment_ids when assigning the batch.

Tests

New regression tests (all passing locally, with the relevant existing suites green):

  • test_membership_acl_bypass.py — low-priv user denied on both paths; an officer with create rights still succeeds.
  • test_cycle_null_entitlement_approval.py — a NULL-state entitlement is treated as unapproved.
  • test_approve_entitlements_program_isolation.py — empty recordset no longer crashes; each entitlement is checked against its own program.
  • test_payment_batch_payment_ids.py — generated batches list their payments.

Verified via ./scripts/test_single_module.sh spp_programs on the affected + neighbouring suites: 60 passed, 0 failed, 0 errors.

Notes / out of scope

  • The membership fix closes the ACL/privilege-escalation layer only; the raw-SQL path still skips per-record audit/source-tracking hooks by design (separate concern).
  • payment_ids remains an independent Many2many (not converted to a true inverse), so any other code that sets batch_id directly must likewise update payment_ids.
  • Two other findings from the same scan (hide-menu re-hide behaviour, and the dedup pre-migration's keep-lowest-id / code-collision risks) are intentionally not included here — they need a product decision and a separate migration change, respectively.

Address four issues surfaced by a security scan of recent performance
refactors in spp_programs:

- Gate bulk_create_memberships with check_access("create") so this public,
  RPC-callable helper no longer lets a low-privileged user bypass ACLs via
  its raw INSERT ... ON CONFLICT path or the sudo() ORM path
  (cycle_membership, program_membership).
- Treat NULL-state entitlements as unapproved in
  _compute_all_entitlements_approved (state IS DISTINCT FROM 'approved'), so
  a cycle containing a NULL-state entitlement is no longer reported as fully
  approved.
- Evaluate the fund balance per program in approve_entitlements and guard the
  empty recordset, so a mixed-program batch no longer approves entitlements
  against the wrong program's funds and no longer crashes on an empty set
  (DefaultCashEntitlementManager and SppCashEntitlementManager).
- Populate the payment batch's payment_ids when assigning batch_id in
  _prepare_payments, so generated batches list their payments.

Add regression tests covering each fix.

@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 addresses several security and functional issues: it fixes an ACL bypass in bulk membership creation by adding explicit create access checks, ensures NULL entitlement states do not bypass approval checks by using IS DISTINCT FROM in SQL queries, isolates fund balance checks per program in mixed-program entitlement batches, and correctly populates the independent payment_ids Many2many field during batch payment creation. However, the reviewer identified a critical issue where self.check_access("create") is used instead of the standard Odoo method self.check_access_rights("create") in both cycle_membership.py and program_membership.py, which will cause runtime AttributeError exceptions.

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.

# raw SQL that bypasses the ORM's ACL checks. Enforce model-level create
# access explicitly so a low-privileged user cannot use it to create
# memberships they could not create through the ORM.
self.check_access("create")

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

In Odoo, there is no standard check_access method on models. The standard and correct method to check model-level access rights is check_access_rights. Using check_access will raise an AttributeError at runtime.

Suggested change
self.check_access("create")
self.check_access_rights("create")

# ORM's ACL checks. Enforce model-level create access explicitly so a
# low-privileged user cannot use it to create memberships they could not
# create through the ORM.
self.check_access("create")

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

In Odoo, there is no standard check_access method on models. The standard and correct method to check model-level access rights is check_access_rights. Using check_access will raise an AttributeError at runtime.

Suggested change
self.check_access("create")
self.check_access_rights("create")

@codecov

codecov Bot commented Jun 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 73.06%. Comparing base (00f1e5a) to head (e78e6a6).
⚠️ Report is 41 commits behind head on 19.0.

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff            @@
##             19.0     #241    +/-   ##
========================================
  Coverage   73.05%   73.06%            
========================================
  Files        1069     1070     +1     
  Lines       62080    62368   +288     
========================================
+ Hits        45351    45567   +216     
- Misses      16729    16801    +72     
Flag Coverage Δ
spp_api_v2 80.33% <ø> (ø)
spp_api_v2_change_request 66.85% <ø> (ø)
spp_api_v2_cycles 71.12% <ø> (ø)
spp_api_v2_data 64.41% <ø> (ø)
spp_api_v2_entitlements 70.19% <ø> (ø)
spp_api_v2_gis 71.52% <ø> (ø)
spp_api_v2_products 66.27% <ø> (ø)
spp_api_v2_service_points 70.94% <ø> (ø)
spp_api_v2_simulation 71.12% <ø> (ø)
spp_api_v2_vocabulary 57.26% <ø> (ø)
spp_audit 72.60% <ø> (ø)
spp_base_common 90.26% <ø> (ø)
spp_case_entitlements 97.61% <ø> (ø)
spp_case_programs 97.14% <ø> (ø)
spp_cel_event 85.11% <ø> (ø)
spp_claim_169 58.11% <ø> (ø)
spp_cr_type_assign_program 91.17% <ø> (ø)
spp_programs 65.24% <100.00%> (+0.39%) ⬆️
spp_registry 86.83% <ø> (ø)
spp_security 66.66% <ø> (ø)

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

Files with missing lines Coverage Δ
spp_programs/models/cycle.py 65.48% <ø> (-0.27%) ⬇️
spp_programs/models/cycle_membership.py 74.41% <100.00%> (+0.30%) ⬆️
...ograms/models/managers/entitlement_manager_base.py 74.41% <100.00%> (+1.13%) ⬆️
...ograms/models/managers/entitlement_manager_cash.py 64.45% <100.00%> (+0.95%) ⬆️
spp_programs/models/managers/payment_manager.py 66.66% <100.00%> (+8.08%) ⬆️
spp_programs/models/program_membership.py 57.51% <ø> (+0.44%) ⬆️

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

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