From 12dfc9c7728682ac10c62d55e425feafb9929462 Mon Sep 17 00:00:00 2001 From: Ken Lewerentz Date: Tue, 25 Aug 2026 14:25:34 +0700 Subject: [PATCH 1/4] fix(security): let Tier-3 registry read reach the registrant form's models MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit spp_registry.group_registry_viewer (Tier-2) implies group_registry_read (Tier-3), so any model granted only to the viewer tier becomes unreadable for a role scoped to the read tier. Several modules that extend the registrant form grant their models to the viewer tier alone, so such a role gets an AccessError merely opening a registrant. Verified per model against a live database rather than by reading ACL files: spp_consent's nine models, spp_programs' spp.cycle and spp.cycle.membership (the entitlement lists render cycle_id) and spp_irrigation's spp.irrigation.asset are genuinely unreachable. res.partner.bank is not, despite being viewer-only here — Odoo core already grants it to base.group_user — so spp_banking needs no change. spp_programs skips 19.0.2.2.2 and .2.2.3, which the security batch-2 branch claims; this assumes that branch merges first. Also mounts docker/postgresql.conf with :ro,z. Without the SELinux relabel flag the database container cannot read it from a fresh git worktree, which breaks scripts/test_single_module.sh there. --- docker-compose.yml | 2 +- spp_consent/README.rst | 9 ++++ spp_consent/__manifest__.py | 2 +- spp_consent/readme/HISTORY.md | 4 ++ spp_consent/security/ir.model.access.csv | 9 ++++ spp_consent/static/description/index.html | 12 ++++- spp_consent/tests/__init__.py | 1 + .../tests/test_registry_read_access.py | 45 +++++++++++++++++++ spp_irrigation/README.rst | 11 +++++ spp_irrigation/__manifest__.py | 2 +- spp_irrigation/readme/HISTORY.md | 4 ++ spp_irrigation/security/ir.model.access.csv | 1 + spp_irrigation/static/description/index.html | 14 +++++- spp_irrigation/tests/__init__.py | 1 + .../tests/test_registry_read_access.py | 38 ++++++++++++++++ spp_programs/README.rst | 8 ++++ spp_programs/__manifest__.py | 2 +- spp_programs/readme/HISTORY.md | 4 ++ spp_programs/security/ir.model.access.csv | 2 + spp_programs/static/description/index.html | 39 +++++++++------- spp_programs/tests/__init__.py | 1 + .../tests/test_registry_read_access.py | 39 ++++++++++++++++ 22 files changed, 229 insertions(+), 21 deletions(-) create mode 100644 spp_consent/tests/test_registry_read_access.py create mode 100644 spp_irrigation/tests/test_registry_read_access.py create mode 100644 spp_programs/tests/test_registry_read_access.py diff --git a/docker-compose.yml b/docker-compose.yml index 4bb9ed1c0..417c0d0a6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -59,7 +59,7 @@ services: POSTGRES_DB: openspp volumes: - postgres_data:/var/lib/postgresql - - ./docker/postgresql.conf:/etc/postgresql/postgresql.conf:ro + - ./docker/postgresql.conf:/etc/postgresql/postgresql.conf:ro,z healthcheck: test: ["CMD-SHELL", "pg_isready -U odoo -d postgres"] interval: 5s diff --git a/spp_consent/README.rst b/spp_consent/README.rst index a025e6cd0..3d3e2b467 100644 --- a/spp_consent/README.rst +++ b/spp_consent/README.rst @@ -193,6 +193,15 @@ Dependencies Changelog ========= +19.0.2.0.2 +~~~~~~~~~~ + +- fix(security): the Tier-3 ``group_registry_read`` group can read the + consent models. This module extends the registrant form, and its + models were granted only to the Tier-2 ``group_registry_viewer`` + group, so a Tier-3-scoped role hit an AccessError opening a + registrant. + 19.0.2.0.1 ~~~~~~~~~~ diff --git a/spp_consent/__manifest__.py b/spp_consent/__manifest__.py index 5c9570502..1fb338285 100644 --- a/spp_consent/__manifest__.py +++ b/spp_consent/__manifest__.py @@ -3,7 +3,7 @@ { "name": "OpenSPP Consent", "category": "OpenSPP", - "version": "19.0.2.0.1", + "version": "19.0.2.0.2", "summary": """DPV-aligned consent management for social protection programs. Implements ISO/IEC TS 27560:2023 consent record information structure diff --git a/spp_consent/readme/HISTORY.md b/spp_consent/readme/HISTORY.md index c9a9bc34c..2f87bfc78 100644 --- a/spp_consent/readme/HISTORY.md +++ b/spp_consent/readme/HISTORY.md @@ -1,3 +1,7 @@ +### 19.0.2.0.2 + +- fix(security): the Tier-3 `group_registry_read` group can read the consent models. This module extends the registrant form, and its models were granted only to the Tier-2 `group_registry_viewer` group, so a Tier-3-scoped role hit an AccessError opening a registrant. + ### 19.0.2.0.1 - fix(views): apply `spp_registry.x2many_no_padding` widget to the Consents list on registrant forms, and hide the table entirely when there are no consents (showing a muted info line instead) — matches the empty-state treatment of read-only / no-create lists elsewhere (#943). diff --git a/spp_consent/security/ir.model.access.csv b/spp_consent/security/ir.model.access.csv index d9ad4ad11..36b0b8efc 100644 --- a/spp_consent/security/ir.model.access.csv +++ b/spp_consent/security/ir.model.access.csv @@ -35,3 +35,12 @@ access_spp_bulk_record_consent_wizard_viewer,Bulk Record Consent Wizard Viewer A access_spp_bulk_record_consent_wizard_officer,Bulk Record Consent Wizard Officer Access,spp_consent.model_spp_bulk_record_consent_wizard,spp_registry.group_registry_officer,1,1,1,0 access_spp_bulk_record_consent_wizard_manager,Bulk Record Consent Wizard Manager Access,spp_consent.model_spp_bulk_record_consent_wizard,spp_registry.group_registry_manager,1,1,1,1 access_spp_bulk_record_consent_wizard_admin,Bulk Record Consent Wizard Admin Access,spp_consent.model_spp_bulk_record_consent_wizard,spp_security.group_spp_admin,1,1,1,1 +access_spp_consent_registry_read,Consent Registry Read Access,spp_consent.model_spp_consent,spp_registry.group_registry_read,1,0,0,0 +access_spp_record_consent_wizard_registry_read,Record Consent Wizard Registry Read Access,spp_consent.model_spp_record_consent_wizard,spp_registry.group_registry_read,1,0,0,0 +access_spp_consent_purpose_registry_read,Consent Purpose Registry Read Access,spp_consent.model_spp_consent_purpose,spp_registry.group_registry_read,1,0,0,0 +access_spp_consent_personal_data_registry_read,Consent Personal Data Registry Read Access,spp_consent.model_spp_consent_personal_data,spp_registry.group_registry_read,1,0,0,0 +access_spp_consent_processing_registry_read,Consent Processing Registry Read Access,spp_consent.model_spp_consent_processing,spp_registry.group_registry_read,1,0,0,0 +access_spp_consent_notice_registry_read,Consent Notice Registry Read Access,spp_consent.model_spp_consent_notice,spp_registry.group_registry_read,1,0,0,0 +access_spp_consent_history_registry_read,Consent History Registry Read Access,spp_consent.model_spp_consent_history,spp_registry.group_registry_read,1,0,0,0 +access_spp_consent_org_type_registry_read,Consent Org Type Registry Read Access,spp_consent.model_spp_consent_org_type,spp_registry.group_registry_read,1,0,0,0 +access_spp_bulk_record_consent_wizard_registry_read,Bulk Record Consent Wizard Registry Read Access,spp_consent.model_spp_bulk_record_consent_wizard,spp_registry.group_registry_read,1,0,0,0 diff --git a/spp_consent/static/description/index.html b/spp_consent/static/description/index.html index 556808ef9..973c6ffe0 100644 --- a/spp_consent/static/description/index.html +++ b/spp_consent/static/description/index.html @@ -589,6 +589,16 @@

Changelog

+

19.0.2.0.2

+ +
+

19.0.2.0.1

-
+

19.0.2.0.0

  • Initial migration to OpenSPP2
  • diff --git a/spp_consent/tests/__init__.py b/spp_consent/tests/__init__.py index c3b086edc..d7a5ba230 100644 --- a/spp_consent/tests/__init__.py +++ b/spp_consent/tests/__init__.py @@ -11,3 +11,4 @@ from . import test_record_consent_wizard from . import test_registrant from . import test_security +from . import test_registry_read_access diff --git a/spp_consent/tests/test_registry_read_access.py b/spp_consent/tests/test_registry_read_access.py new file mode 100644 index 000000000..3aebadb9d --- /dev/null +++ b/spp_consent/tests/test_registry_read_access.py @@ -0,0 +1,45 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +"""Tier-3 ``group_registry_read`` must cover this module's registrant-form models. + +``spp_registry.group_registry_viewer`` (Tier-2) implies ``group_registry_read`` +(Tier-3), so any ACL granted only to the viewer tier disappears for a role +scoped to the read tier. ``spp_consent/views/registrant_view.xml`` extends the registrant form, +so a read-tier role that opens a registrant hits an AccessError unless the +Tier-3 group carries these models too. +""" + +from odoo.tests import TransactionCase, tagged + +_MODELS = [ + "spp.consent", + "spp.consent.purpose", + "spp.consent.personal.data", + "spp.consent.processing", + "spp.consent.notice", + "spp.consent.history", + "spp.consent.org.type", + "spp.record.consent.wizard", + "spp.bulk.record.consent.wizard", +] + + +@tagged("post_install", "-at_install") +class TestRegistryReadAccess(TransactionCase): + def test_tier3_registry_read_covers_registrant_form_models(self): + user = self.env["res.users"].create( + { + "name": "Tier3 Reader", + "login": "tier3_reader_spp_consent", + "email": "tier3_reader_spp_consent@example.com", + "group_ids": [ + (4, self.env.ref("base.group_user").id), + (4, self.env.ref("spp_registry.group_registry_read").id), + ], + } + ) + Access = self.env["ir.model.access"].with_user(user) + missing = sorted(m for m in _MODELS if not Access.check(m, "read", raise_exception=False)) + self.assertFalse( + missing, + "Tier-3 group_registry_read cannot read registrant-form models: " + ", ".join(missing), + ) diff --git a/spp_irrigation/README.rst b/spp_irrigation/README.rst index 06303904c..e2f7304a1 100644 --- a/spp_irrigation/README.rst +++ b/spp_irrigation/README.rst @@ -100,6 +100,17 @@ Dependencies Changelog ========= +19.0.2.1.1 +~~~~~~~~~~ + +- fix(security): the Tier-3 ``group_registry_read`` group can read + ``spp.irrigation.asset``. The registrant form renders + ``irrigation_asset_ids``, and the model was granted only to the Tier-2 + ``group_registry_viewer`` group. Note the pre-existing + ``access_spp_irrigation_asset_registry_read`` entry is misnamed — it + grants the Tier-2 viewer group — so the new entry uses a distinct + identifier rather than renaming it and orphaning the old one. + 19.0.2.1.0 ~~~~~~~~~~ diff --git a/spp_irrigation/__manifest__.py b/spp_irrigation/__manifest__.py index 99b64208c..8547febe4 100644 --- a/spp_irrigation/__manifest__.py +++ b/spp_irrigation/__manifest__.py @@ -5,7 +5,7 @@ { "name": "OpenSPP Irrigation", "category": "OpenSPP", - "version": "19.0.2.1.0", + "version": "19.0.2.1.1", "sequence": 1, "author": "OpenSPP.org", "website": "https://github.com/OpenSPP/OpenSPP2", diff --git a/spp_irrigation/readme/HISTORY.md b/spp_irrigation/readme/HISTORY.md index 8a595c3ed..e8bb85f25 100644 --- a/spp_irrigation/readme/HISTORY.md +++ b/spp_irrigation/readme/HISTORY.md @@ -1,3 +1,7 @@ +### 19.0.2.1.1 + +- fix(security): the Tier-3 `group_registry_read` group can read `spp.irrigation.asset`. The registrant form renders `irrigation_asset_ids`, and the model was granted only to the Tier-2 `group_registry_viewer` group. Note the pre-existing `access_spp_irrigation_asset_registry_read` entry is misnamed — it grants the Tier-2 viewer group — so the new entry uses a distinct identifier rather than renaming it and orphaning the old one. + ### 19.0.2.1.0 - feat(views): add an "Irrigation" tab on the farm (group) form so per-farm irrigation assets are reachable without leaving the farm record; backed by a new `irrigation_asset_ids` One2many on `res.partner` (inverse of the existing `farm_id`) diff --git a/spp_irrigation/security/ir.model.access.csv b/spp_irrigation/security/ir.model.access.csv index 27bb5ed85..d50d8b86b 100644 --- a/spp_irrigation/security/ir.model.access.csv +++ b/spp_irrigation/security/ir.model.access.csv @@ -2,3 +2,4 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_spp_irrigation_asset,SPP Irrigation Asset Access,model_spp_irrigation_asset,spp_irrigation.group_irrigation_manager,1,1,1,1 access_spp_irrigation_asset_registry_read,SPP Irrigation Asset Read,model_spp_irrigation_asset,spp_registry.group_registry_viewer,1,0,0,0 +access_spp_irrigation_asset_tier3_read,SPP Irrigation Asset Registry Read,model_spp_irrigation_asset,spp_registry.group_registry_read,1,0,0,0 diff --git a/spp_irrigation/static/description/index.html b/spp_irrigation/static/description/index.html index 355f9974f..f16779fc5 100644 --- a/spp_irrigation/static/description/index.html +++ b/spp_irrigation/static/description/index.html @@ -468,6 +468,18 @@

    Changelog

+

19.0.2.1.1

+ +
+

19.0.2.1.0

-
+

19.0.2.0.0

  • Initial migration to OpenSPP2
  • diff --git a/spp_irrigation/tests/__init__.py b/spp_irrigation/tests/__init__.py index 0986f84bd..d9c16d65b 100644 --- a/spp_irrigation/tests/__init__.py +++ b/spp_irrigation/tests/__init__.py @@ -1,3 +1,4 @@ # Part of OpenSPP. See LICENSE file for full copyright and licensing details. from . import test_irrigation_asset from . import test_irrigation_edge_cases +from . import test_registry_read_access diff --git a/spp_irrigation/tests/test_registry_read_access.py b/spp_irrigation/tests/test_registry_read_access.py new file mode 100644 index 000000000..ee21a57e2 --- /dev/null +++ b/spp_irrigation/tests/test_registry_read_access.py @@ -0,0 +1,38 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +"""Tier-3 ``group_registry_read`` must cover this module's registrant-form models. + +``spp_registry.group_registry_viewer`` (Tier-2) implies ``group_registry_read`` +(Tier-3), so any ACL granted only to the viewer tier disappears for a role +scoped to the read tier. ``spp_irrigation/views/irrigation_view.xml`` renders +``irrigation_asset_ids`` on the registrant form, +so a read-tier role that opens a registrant hits an AccessError unless the +Tier-3 group carries these models too. +""" + +from odoo.tests import TransactionCase, tagged + +_MODELS = [ + "spp.irrigation.asset", +] + + +@tagged("post_install", "-at_install") +class TestRegistryReadAccess(TransactionCase): + def test_tier3_registry_read_covers_registrant_form_models(self): + user = self.env["res.users"].create( + { + "name": "Tier3 Reader", + "login": "tier3_reader_spp_irrigation", + "email": "tier3_reader_spp_irrigation@example.com", + "group_ids": [ + (4, self.env.ref("base.group_user").id), + (4, self.env.ref("spp_registry.group_registry_read").id), + ], + } + ) + Access = self.env["ir.model.access"].with_user(user) + missing = sorted(m for m in _MODELS if not Access.check(m, "read", raise_exception=False)) + self.assertFalse( + missing, + "Tier-3 group_registry_read cannot read registrant-form models: " + ", ".join(missing), + ) diff --git a/spp_programs/README.rst b/spp_programs/README.rst index b79966144..68db5c9b1 100644 --- a/spp_programs/README.rst +++ b/spp_programs/README.rst @@ -254,6 +254,14 @@ Dependencies Changelog ========= +19.0.2.2.4 +~~~~~~~~~~ + +- fix(security): the Tier-3 ``group_registry_read`` group can read + ``spp.cycle`` and ``spp.cycle.membership``. The registrant form's + entitlement lists render ``cycle_id``, and both models were granted + only to the Tier-2 ``group_registry_viewer`` group. + 19.0.2.2.1 ~~~~~~~~~~ diff --git a/spp_programs/__manifest__.py b/spp_programs/__manifest__.py index 48590c716..03d193318 100644 --- a/spp_programs/__manifest__.py +++ b/spp_programs/__manifest__.py @@ -4,7 +4,7 @@ "name": "OpenSPP Programs", "summary": "Manage programs, cycles, beneficiary enrollment, entitlements (cash and in-kind), payments, and fund tracking for social protection.", "category": "OpenSPP/Core", - "version": "19.0.2.2.1", + "version": "19.0.2.2.4", "sequence": 1, "author": "OpenSPP.org", "website": "https://github.com/OpenSPP/OpenSPP2", diff --git a/spp_programs/readme/HISTORY.md b/spp_programs/readme/HISTORY.md index 37046e6ba..9d6ddebcf 100644 --- a/spp_programs/readme/HISTORY.md +++ b/spp_programs/readme/HISTORY.md @@ -1,3 +1,7 @@ +### 19.0.2.2.4 + +- fix(security): the Tier-3 `group_registry_read` group can read `spp.cycle` and `spp.cycle.membership`. The registrant form's entitlement lists render `cycle_id`, and both models were granted only to the Tier-2 `group_registry_viewer` group. + ### 19.0.2.2.1 - fix(spp_programs): stop Enroll Eligible undoing a deliberate pause. A paused membership is now left alone wherever eligibility is re-run — the enrol pass, the disenrol sweep that would otherwise have moved it to Not Eligible, and the per-membership methods reachable over RPC. Pausing is a decision that only Resume reverses (#1117) diff --git a/spp_programs/security/ir.model.access.csv b/spp_programs/security/ir.model.access.csv index baced8b9a..e29908e0f 100644 --- a/spp_programs/security/ir.model.access.csv +++ b/spp_programs/security/ir.model.access.csv @@ -404,3 +404,5 @@ access_spp_prepare_entitlement_confirm_wizard_validator,Prepare Entitlement Conf access_spp_program_membership_exit_wizard_officer,Program Membership Exit Wizard Officer Access,spp_programs.model_spp_program_membership_exit_wizard,spp_programs.group_programs_officer,1,1,1,0 access_spp_program_membership_exit_wizard_manager,Program Membership Exit Wizard Manager Access,spp_programs.model_spp_program_membership_exit_wizard,spp_programs.group_programs_manager,1,1,1,1 access_spp_program_membership_exit_wizard_admin,Program Membership Exit Wizard Admin Access,spp_programs.model_spp_program_membership_exit_wizard,spp_security.group_spp_admin,1,1,1,1 +access_spp_cycle_registry_registry_read,Cycle Registry Registry Read Read,spp_programs.model_spp_cycle,spp_registry.group_registry_read,1,0,0,0 +access_spp_cycle_membership_registry_registry_read,Cycle Membership Registry Registry Read Read,spp_programs.model_spp_cycle_membership,spp_registry.group_registry_read,1,0,0,0 diff --git a/spp_programs/static/description/index.html b/spp_programs/static/description/index.html index 71163ce80..6fe4697d3 100644 --- a/spp_programs/static/description/index.html +++ b/spp_programs/static/description/index.html @@ -658,6 +658,15 @@

    Changelog

+

19.0.2.2.4

+ +
+

19.0.2.2.1

-
+

19.0.2.1.3

  • fix(security): align Program Viewer / Validator / Cycle Approver roles @@ -686,7 +695,7 @@

    19.0.2.1.3

    cross-references — only the dedicated top-level menu disappears.
-
+

19.0.2.1.2

  • fix(security): add global ir.rule records on @@ -700,7 +709,7 @@

    19.0.2.1.2

    no-op for users with no center areas (global roles).
-
+

19.0.2.1.1

  • fix(views): apply spp_registry.x2many_no_padding widget to the @@ -709,7 +718,7 @@

    19.0.2.1.1

    19 inserts on inline list-in-form views (#943).
-
+

19.0.2.0.11

  • Fix TypeError: 'NoneType' object is not iterable when clicking @@ -720,7 +729,7 @@

    19.0.2.0.11

    omit the state filter instead of crashing on tuple(None)
-
+

19.0.2.0.10

  • Increase parallel-safe channel limits (cycle, eligibility_manager, @@ -733,7 +742,7 @@

    19.0.2.0.10

    submission on double-click
-
+

19.0.2.0.9

  • Add context flags (skip_registrant_statistics, @@ -746,7 +755,7 @@

    19.0.2.0.9

    _compute_has_members
-
+

19.0.2.0.8

  • Replace OFFSET pagination with NTILE-based ID-range batching in all @@ -757,7 +766,7 @@

    19.0.2.0.8

    program and cycle
-
+

19.0.2.0.7

  • Bulk membership creation using raw SQL INSERT ON CONFLICT DO NOTHING @@ -766,7 +775,7 @@

    19.0.2.0.7

    _add_beneficiaries with bulk SQL path
-
+

19.0.2.0.6

  • Remove unused entitlement_base_model.py (dead code, never imported)
  • @@ -775,34 +784,34 @@

    19.0.2.0.6

    payment, and fund tests (172 → 492 tests)
-
+

19.0.2.0.5

  • Batch create entitlements and payments instead of one-by-one ORM creates
-
+

19.0.2.0.4

  • Fetch fund balance once per approval batch instead of per entitlement
-
+

19.0.2.0.3

  • Replace cycle computed fields (total_amount, entitlements_count, approval flags) with SQL aggregation queries
-
+

19.0.2.0.2

  • Add composite indexes for frequent query patterns on entitlements and program memberships
-
+

19.0.2.0.1

  • Replace Python-level uniqueness checks with SQL UNIQUE constraints for @@ -811,7 +820,7 @@

    19.0.2.0.1

    constraint creation
-
+

19.0.2.0.0

  • Initial migration to OpenSPP2
  • diff --git a/spp_programs/tests/__init__.py b/spp_programs/tests/__init__.py index 15dc1cbe6..3c853492b 100644 --- a/spp_programs/tests/__init__.py +++ b/spp_programs/tests/__init__.py @@ -43,3 +43,4 @@ from . import test_cycle_null_entitlement_approval from . import test_approve_entitlements_program_isolation from . import test_payment_batch_payment_ids +from . import test_registry_read_access diff --git a/spp_programs/tests/test_registry_read_access.py b/spp_programs/tests/test_registry_read_access.py new file mode 100644 index 000000000..2bc68ff3b --- /dev/null +++ b/spp_programs/tests/test_registry_read_access.py @@ -0,0 +1,39 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +"""Tier-3 ``group_registry_read`` must cover this module's registrant-form models. + +``spp_registry.group_registry_viewer`` (Tier-2) implies ``group_registry_read`` +(Tier-3), so any ACL granted only to the viewer tier disappears for a role +scoped to the read tier. ``spp_programs/views/registrant_view.xml`` renders ``cycle_id`` in the +entitlement lists on the registrant form, +so a read-tier role that opens a registrant hits an AccessError unless the +Tier-3 group carries these models too. +""" + +from odoo.tests import TransactionCase, tagged + +_MODELS = [ + "spp.cycle", + "spp.cycle.membership", +] + + +@tagged("post_install", "-at_install") +class TestRegistryReadAccess(TransactionCase): + def test_tier3_registry_read_covers_registrant_form_models(self): + user = self.env["res.users"].create( + { + "name": "Tier3 Reader", + "login": "tier3_reader_spp_programs", + "email": "tier3_reader_spp_programs@example.com", + "group_ids": [ + (4, self.env.ref("base.group_user").id), + (4, self.env.ref("spp_registry.group_registry_read").id), + ], + } + ) + Access = self.env["ir.model.access"].with_user(user) + missing = sorted(m for m in _MODELS if not Access.check(m, "read", raise_exception=False)) + self.assertFalse( + missing, + "Tier-3 group_registry_read cannot read registrant-form models: " + ", ".join(missing), + ) From 07f792a0aa4a77fe5373e8f1c2fef5dab079de39 Mon Sep 17 00:00:00 2001 From: Ken Lewerentz Date: Wed, 26 Aug 2026 13:49:08 +0700 Subject: [PATCH 2/4] chore: move spp_programs to 19.0.2.3.4 The security batch-2 branch (#422) now also takes 19.0.2.3.3, for guarding the operation lock on create as well as write, so this branch moves up one to stay above it. Still assumes #422 merges first. --- spp_programs/README.rst | 2 +- spp_programs/__manifest__.py | 2 +- spp_programs/readme/HISTORY.md | 2 +- spp_programs/static/description/index.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spp_programs/README.rst b/spp_programs/README.rst index 91e408a02..953b11664 100644 --- a/spp_programs/README.rst +++ b/spp_programs/README.rst @@ -254,7 +254,7 @@ Dependencies Changelog ========= -19.0.2.3.3 +19.0.2.3.4 ~~~~~~~~~~ - fix(security): the Tier-3 ``group_registry_read`` group can read diff --git a/spp_programs/__manifest__.py b/spp_programs/__manifest__.py index 4e4cda8b8..cdb136049 100644 --- a/spp_programs/__manifest__.py +++ b/spp_programs/__manifest__.py @@ -4,7 +4,7 @@ "name": "OpenSPP Programs", "summary": "Manage programs, cycles, beneficiary enrollment, entitlements (cash and in-kind), payments, and fund tracking for social protection.", "category": "OpenSPP/Core", - "version": "19.0.2.3.3", + "version": "19.0.2.3.4", "sequence": 1, "author": "OpenSPP.org", "website": "https://github.com/OpenSPP/OpenSPP2", diff --git a/spp_programs/readme/HISTORY.md b/spp_programs/readme/HISTORY.md index bfb20bd87..2ee7755a1 100644 --- a/spp_programs/readme/HISTORY.md +++ b/spp_programs/readme/HISTORY.md @@ -1,4 +1,4 @@ -### 19.0.2.3.3 +### 19.0.2.3.4 - fix(security): the Tier-3 `group_registry_read` group can read `spp.cycle` and `spp.cycle.membership`. The registrant form's entitlement lists render `cycle_id`, and both models were granted only to the Tier-2 `group_registry_viewer` group. diff --git a/spp_programs/static/description/index.html b/spp_programs/static/description/index.html index 73b1c852e..42bf4cd1f 100644 --- a/spp_programs/static/description/index.html +++ b/spp_programs/static/description/index.html @@ -658,7 +658,7 @@

    Changelog

-

19.0.2.3.3

+

19.0.2.3.4

  • fix(security): the Tier-3 group_registry_read group can read spp.cycle and spp.cycle.membership. The registrant form’s From 257c8efa8ec19e8c0d024fa2ec0739cb9f03bf88 Mon Sep 17 00:00:00 2001 From: Ken Lewerentz Date: Wed, 26 Aug 2026 15:02:02 +0700 Subject: [PATCH 3/4] chore: restore spp_change_request_v2 README to the generator's output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Regenerating the README fragments after merging 19.0 rewrote every module, not just the one this branch touches, and the local renderer lays RST table columns out one character wider than CI does. Only spp_programs was corrected, so an unrelated spp_change_request_v2 diff rode along. Reverted to 19.0's version — this branch does not touch that module. --- spp_change_request_v2/README.rst | 32 +++++++++---------- .../static/description/index.html | 4 +-- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/spp_change_request_v2/README.rst b/spp_change_request_v2/README.rst index 30573661f..4ee4aba47 100644 --- a/spp_change_request_v2/README.rst +++ b/spp_change_request_v2/README.rst @@ -752,22 +752,22 @@ Methods available for override on detail models (all inherited from Related fields available on all detail models (from ``spp.cr.detail.base``): -+----------------------------+-----------+------------------------------------------------------------+ -| Field | Type | Source | -+============================+===========+============================================================+ -| ``change_request_id`` | Many2one | Direct link to parent CR | -+----------------------------+-----------+------------------------------------------------------------+ -| ``registrant_id`` | Many2one | ``change_request_id.registrant_id`` | -+----------------------------+-----------+------------------------------------------------------------+ -| ``approval_state`` | Selection | ``change_request_id.approval_state`` | -+----------------------------+-----------+------------------------------------------------------------+ -| ``is_applied`` | Boolean | ``change_request_id.is_applied`` | -+----------------------------+-----------+------------------------------------------------------------+ -| ``use_dynamic_approval`` | Boolean | ``change_request_id.request_type_id.use_dynamic_approval`` | -+----------------------------+-----------+------------------------------------------------------------+ -| ``field_to_modify`` | Selection | Dynamic field selector (populated by | -| | | ``_get_field_to_modify_selection``) | -+----------------------------+-----------+------------------------------------------------------------+ ++--------------------------+-----------+------------------------------------------------------------+ +| Field | Type | Source | ++==========================+===========+============================================================+ +| ``change_request_id`` | Many2one | Direct link to parent CR | ++--------------------------+-----------+------------------------------------------------------------+ +| ``registrant_id`` | Many2one | ``change_request_id.registrant_id`` | ++--------------------------+-----------+------------------------------------------------------------+ +| ``approval_state`` | Selection | ``change_request_id.approval_state`` | ++--------------------------+-----------+------------------------------------------------------------+ +| ``is_applied`` | Boolean | ``change_request_id.is_applied`` | ++--------------------------+-----------+------------------------------------------------------------+ +| ``use_dynamic_approval`` | Boolean | ``change_request_id.request_type_id.use_dynamic_approval`` | ++--------------------------+-----------+------------------------------------------------------------+ +| ``field_to_modify`` | Selection | Dynamic field selector (populated by | +| | | ``_get_field_to_modify_selection``) | ++--------------------------+-----------+------------------------------------------------------------+ CR Type Fields Reference ~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/spp_change_request_v2/static/description/index.html b/spp_change_request_v2/static/description/index.html index 8aa4bcc72..22feeaea5 100644 --- a/spp_change_request_v2/static/description/index.html +++ b/spp_change_request_v2/static/description/index.html @@ -1160,9 +1160,9 @@

    Methods Reference

    spp.cr.detail.base):

    -+-+ From 7a46a5a35d2b9cbfdaaef119eed20584a5ad5da5 Mon Sep 17 00:00:00 2001 From: Ken Lewerentz Date: Thu, 27 Aug 2026 09:52:27 +0700 Subject: [PATCH 4/4] =?UTF-8?q?fix(security):=20address=20review=20?= =?UTF-8?q?=E2=80=94=20drop=20the=20wizard=20grants,=20fix=20the=20ACL=20x?= =?UTF-8?q?mlids?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two corrections from review. The two consent wizard grants are dropped. Neither wizard is a registrant-form dependency by this branch's own criterion — both Record Consent entry points are gated to the officer and manager tiers, and read without create cannot open a wizard anyway — so the grant buys a Tier-3 user nothing. It is not inert elsewhere: the bulk wizard's action binds to res.partner with no group_ids, and get_bindings filters only on read access to the action's model, so the row would surface "Bulk Record Consent" in the registrant list for read-only users who then fail on click. Odoo also enforces no per-user isolation on transient models, so a model-level read grant exposes other users' in-flight wizard rows. Restoring pre-#353 parity here would have restored a bug. The irrigation ACL identifiers now follow the documented access_{model}_{group} convention: the misnamed pre-existing row becomes ..._registry_viewer, and the new Tier-3 row takes ..._registry_read. The claim that renaming would orphan the old xmlid was wrong — CSV rows load with noupdate false, and _process_end removes exactly those identifiers when a reloaded module no longer declares them — so the rationale is dropped from the changelog rather than restated. Also from review: the spp.cycle.membership reason now names the real dependency (latest_cycle_state, computed by searching that model as the acting user) rather than cycle_id; the consent changelog records why the wizards are excluded; Command.link() replaces (4, id) tuples per the Odoo 19 compatibility guidance; spp_consent/tests/__init__.py is alphabetical again; and ./e2e/reports takes ,z, the last bind mount without it. --- spp_consent/README.rst | 11 +++++++---- spp_consent/readme/HISTORY.md | 2 +- spp_consent/security/ir.model.access.csv | 2 -- spp_consent/static/description/index.html | 11 +++++++---- spp_consent/tests/__init__.py | 2 +- spp_consent/tests/test_registry_read_access.py | 7 +++---- spp_irrigation/README.rst | 5 +---- spp_irrigation/readme/HISTORY.md | 2 +- spp_irrigation/security/ir.model.access.csv | 4 ++-- spp_irrigation/static/description/index.html | 5 +---- spp_irrigation/tests/test_registry_read_access.py | 5 +++-- spp_programs/README.rst | 8 +++++--- spp_programs/readme/HISTORY.md | 2 +- spp_programs/static/description/index.html | 8 +++++--- spp_programs/tests/test_registry_read_access.py | 5 +++-- 15 files changed, 41 insertions(+), 38 deletions(-) diff --git a/spp_consent/README.rst b/spp_consent/README.rst index 3d3e2b467..ac3e8493b 100644 --- a/spp_consent/README.rst +++ b/spp_consent/README.rst @@ -197,10 +197,13 @@ Changelog ~~~~~~~~~~ - fix(security): the Tier-3 ``group_registry_read`` group can read the - consent models. This module extends the registrant form, and its - models were granted only to the Tier-2 ``group_registry_viewer`` - group, so a Tier-3-scoped role hit an AccessError opening a - registrant. + consent models the registrant form depends on. This module extends + that form, and its models were granted only to the Tier-2 + ``group_registry_viewer`` group, so a Tier-3-scoped role hit an + AccessError opening a registrant. The two wizard models are + deliberately not included: their entry points are restricted to the + officer and manager tiers, so read access would grant nothing usable + while exposing other users' in-progress wizard rows. 19.0.2.0.1 ~~~~~~~~~~ diff --git a/spp_consent/readme/HISTORY.md b/spp_consent/readme/HISTORY.md index 2f87bfc78..425bb25aa 100644 --- a/spp_consent/readme/HISTORY.md +++ b/spp_consent/readme/HISTORY.md @@ -1,6 +1,6 @@ ### 19.0.2.0.2 -- fix(security): the Tier-3 `group_registry_read` group can read the consent models. This module extends the registrant form, and its models were granted only to the Tier-2 `group_registry_viewer` group, so a Tier-3-scoped role hit an AccessError opening a registrant. +- fix(security): the Tier-3 `group_registry_read` group can read the consent models the registrant form depends on. This module extends that form, and its models were granted only to the Tier-2 `group_registry_viewer` group, so a Tier-3-scoped role hit an AccessError opening a registrant. The two wizard models are deliberately not included: their entry points are restricted to the officer and manager tiers, so read access would grant nothing usable while exposing other users' in-progress wizard rows. ### 19.0.2.0.1 diff --git a/spp_consent/security/ir.model.access.csv b/spp_consent/security/ir.model.access.csv index 36b0b8efc..8ea4fcaae 100644 --- a/spp_consent/security/ir.model.access.csv +++ b/spp_consent/security/ir.model.access.csv @@ -36,11 +36,9 @@ access_spp_bulk_record_consent_wizard_officer,Bulk Record Consent Wizard Officer access_spp_bulk_record_consent_wizard_manager,Bulk Record Consent Wizard Manager Access,spp_consent.model_spp_bulk_record_consent_wizard,spp_registry.group_registry_manager,1,1,1,1 access_spp_bulk_record_consent_wizard_admin,Bulk Record Consent Wizard Admin Access,spp_consent.model_spp_bulk_record_consent_wizard,spp_security.group_spp_admin,1,1,1,1 access_spp_consent_registry_read,Consent Registry Read Access,spp_consent.model_spp_consent,spp_registry.group_registry_read,1,0,0,0 -access_spp_record_consent_wizard_registry_read,Record Consent Wizard Registry Read Access,spp_consent.model_spp_record_consent_wizard,spp_registry.group_registry_read,1,0,0,0 access_spp_consent_purpose_registry_read,Consent Purpose Registry Read Access,spp_consent.model_spp_consent_purpose,spp_registry.group_registry_read,1,0,0,0 access_spp_consent_personal_data_registry_read,Consent Personal Data Registry Read Access,spp_consent.model_spp_consent_personal_data,spp_registry.group_registry_read,1,0,0,0 access_spp_consent_processing_registry_read,Consent Processing Registry Read Access,spp_consent.model_spp_consent_processing,spp_registry.group_registry_read,1,0,0,0 access_spp_consent_notice_registry_read,Consent Notice Registry Read Access,spp_consent.model_spp_consent_notice,spp_registry.group_registry_read,1,0,0,0 access_spp_consent_history_registry_read,Consent History Registry Read Access,spp_consent.model_spp_consent_history,spp_registry.group_registry_read,1,0,0,0 access_spp_consent_org_type_registry_read,Consent Org Type Registry Read Access,spp_consent.model_spp_consent_org_type,spp_registry.group_registry_read,1,0,0,0 -access_spp_bulk_record_consent_wizard_registry_read,Bulk Record Consent Wizard Registry Read Access,spp_consent.model_spp_bulk_record_consent_wizard,spp_registry.group_registry_read,1,0,0,0 diff --git a/spp_consent/static/description/index.html b/spp_consent/static/description/index.html index 973c6ffe0..6ee4b98fb 100644 --- a/spp_consent/static/description/index.html +++ b/spp_consent/static/description/index.html @@ -592,10 +592,13 @@

    Changelog

    19.0.2.0.2

    • fix(security): the Tier-3 group_registry_read group can read the -consent models. This module extends the registrant form, and its -models were granted only to the Tier-2 group_registry_viewer -group, so a Tier-3-scoped role hit an AccessError opening a -registrant.
    • +consent models the registrant form depends on. This module extends +that form, and its models were granted only to the Tier-2 +group_registry_viewer group, so a Tier-3-scoped role hit an +AccessError opening a registrant. The two wizard models are +deliberately not included: their entry points are restricted to the +officer and manager tiers, so read access would grant nothing usable +while exposing other users’ in-progress wizard rows.
    diff --git a/spp_consent/tests/__init__.py b/spp_consent/tests/__init__.py index d7a5ba230..1d8414783 100644 --- a/spp_consent/tests/__init__.py +++ b/spp_consent/tests/__init__.py @@ -10,5 +10,5 @@ from . import test_privacy_notices from . import test_record_consent_wizard from . import test_registrant -from . import test_security from . import test_registry_read_access +from . import test_security diff --git a/spp_consent/tests/test_registry_read_access.py b/spp_consent/tests/test_registry_read_access.py index 3aebadb9d..ca246d97a 100644 --- a/spp_consent/tests/test_registry_read_access.py +++ b/spp_consent/tests/test_registry_read_access.py @@ -8,6 +8,7 @@ Tier-3 group carries these models too. """ +from odoo import Command from odoo.tests import TransactionCase, tagged _MODELS = [ @@ -18,8 +19,6 @@ "spp.consent.notice", "spp.consent.history", "spp.consent.org.type", - "spp.record.consent.wizard", - "spp.bulk.record.consent.wizard", ] @@ -32,8 +31,8 @@ def test_tier3_registry_read_covers_registrant_form_models(self): "login": "tier3_reader_spp_consent", "email": "tier3_reader_spp_consent@example.com", "group_ids": [ - (4, self.env.ref("base.group_user").id), - (4, self.env.ref("spp_registry.group_registry_read").id), + Command.link(self.env.ref("base.group_user").id), + Command.link(self.env.ref("spp_registry.group_registry_read").id), ], } ) diff --git a/spp_irrigation/README.rst b/spp_irrigation/README.rst index e2f7304a1..b7179bdc1 100644 --- a/spp_irrigation/README.rst +++ b/spp_irrigation/README.rst @@ -106,10 +106,7 @@ Changelog - fix(security): the Tier-3 ``group_registry_read`` group can read ``spp.irrigation.asset``. The registrant form renders ``irrigation_asset_ids``, and the model was granted only to the Tier-2 - ``group_registry_viewer`` group. Note the pre-existing - ``access_spp_irrigation_asset_registry_read`` entry is misnamed — it - grants the Tier-2 viewer group — so the new entry uses a distinct - identifier rather than renaming it and orphaning the old one. + ``group_registry_viewer`` group. 19.0.2.1.0 ~~~~~~~~~~ diff --git a/spp_irrigation/readme/HISTORY.md b/spp_irrigation/readme/HISTORY.md index e8bb85f25..ebe962bb6 100644 --- a/spp_irrigation/readme/HISTORY.md +++ b/spp_irrigation/readme/HISTORY.md @@ -1,6 +1,6 @@ ### 19.0.2.1.1 -- fix(security): the Tier-3 `group_registry_read` group can read `spp.irrigation.asset`. The registrant form renders `irrigation_asset_ids`, and the model was granted only to the Tier-2 `group_registry_viewer` group. Note the pre-existing `access_spp_irrigation_asset_registry_read` entry is misnamed — it grants the Tier-2 viewer group — so the new entry uses a distinct identifier rather than renaming it and orphaning the old one. +- fix(security): the Tier-3 `group_registry_read` group can read `spp.irrigation.asset`. The registrant form renders `irrigation_asset_ids`, and the model was granted only to the Tier-2 `group_registry_viewer` group. ### 19.0.2.1.0 diff --git a/spp_irrigation/security/ir.model.access.csv b/spp_irrigation/security/ir.model.access.csv index d50d8b86b..31b4bce2b 100644 --- a/spp_irrigation/security/ir.model.access.csv +++ b/spp_irrigation/security/ir.model.access.csv @@ -1,5 +1,5 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_spp_irrigation_asset,SPP Irrigation Asset Access,model_spp_irrigation_asset,spp_irrigation.group_irrigation_manager,1,1,1,1 -access_spp_irrigation_asset_registry_read,SPP Irrigation Asset Read,model_spp_irrigation_asset,spp_registry.group_registry_viewer,1,0,0,0 -access_spp_irrigation_asset_tier3_read,SPP Irrigation Asset Registry Read,model_spp_irrigation_asset,spp_registry.group_registry_read,1,0,0,0 +access_spp_irrigation_asset_registry_viewer,SPP Irrigation Asset Viewer Read,model_spp_irrigation_asset,spp_registry.group_registry_viewer,1,0,0,0 +access_spp_irrigation_asset_registry_read,SPP Irrigation Asset Registry Read,model_spp_irrigation_asset,spp_registry.group_registry_read,1,0,0,0 diff --git a/spp_irrigation/static/description/index.html b/spp_irrigation/static/description/index.html index f16779fc5..d80c18fb9 100644 --- a/spp_irrigation/static/description/index.html +++ b/spp_irrigation/static/description/index.html @@ -473,10 +473,7 @@

    19.0.2.1.1

  • fix(security): the Tier-3 group_registry_read group can read spp.irrigation.asset. The registrant form renders irrigation_asset_ids, and the model was granted only to the Tier-2 -group_registry_viewer group. Note the pre-existing -access_spp_irrigation_asset_registry_read entry is misnamed — it -grants the Tier-2 viewer group — so the new entry uses a distinct -identifier rather than renaming it and orphaning the old one.
  • +group_registry_viewer group.
    diff --git a/spp_irrigation/tests/test_registry_read_access.py b/spp_irrigation/tests/test_registry_read_access.py index ee21a57e2..22c39774a 100644 --- a/spp_irrigation/tests/test_registry_read_access.py +++ b/spp_irrigation/tests/test_registry_read_access.py @@ -9,6 +9,7 @@ Tier-3 group carries these models too. """ +from odoo import Command from odoo.tests import TransactionCase, tagged _MODELS = [ @@ -25,8 +26,8 @@ def test_tier3_registry_read_covers_registrant_form_models(self): "login": "tier3_reader_spp_irrigation", "email": "tier3_reader_spp_irrigation@example.com", "group_ids": [ - (4, self.env.ref("base.group_user").id), - (4, self.env.ref("spp_registry.group_registry_read").id), + Command.link(self.env.ref("base.group_user").id), + Command.link(self.env.ref("spp_registry.group_registry_read").id), ], } ) diff --git a/spp_programs/README.rst b/spp_programs/README.rst index 434835bb6..9436f7903 100644 --- a/spp_programs/README.rst +++ b/spp_programs/README.rst @@ -258,9 +258,11 @@ Changelog ~~~~~~~~~~ - fix(security): the Tier-3 ``group_registry_read`` group can read - ``spp.cycle`` and ``spp.cycle.membership``. The registrant form's - entitlement lists render ``cycle_id``, and both models were granted - only to the Tier-2 ``group_registry_viewer`` group. + ``spp.cycle`` and ``spp.cycle.membership``. Both were granted only to + the Tier-2 ``group_registry_viewer`` group, yet the registrant form + depends on both: the entitlement lists render ``cycle_id``, and the + membership list renders ``latest_cycle_state``, which is computed by + searching ``spp.cycle.membership`` as the acting user. 19.0.2.3.3 ~~~~~~~~~~ diff --git a/spp_programs/readme/HISTORY.md b/spp_programs/readme/HISTORY.md index e2998d1a1..02a0af95c 100644 --- a/spp_programs/readme/HISTORY.md +++ b/spp_programs/readme/HISTORY.md @@ -1,6 +1,6 @@ ### 19.0.2.3.4 -- fix(security): the Tier-3 `group_registry_read` group can read `spp.cycle` and `spp.cycle.membership`. The registrant form's entitlement lists render `cycle_id`, and both models were granted only to the Tier-2 `group_registry_viewer` group. +- fix(security): the Tier-3 `group_registry_read` group can read `spp.cycle` and `spp.cycle.membership`. Both were granted only to the Tier-2 `group_registry_viewer` group, yet the registrant form depends on both: the entitlement lists render `cycle_id`, and the membership list renders `latest_cycle_state`, which is computed by searching `spp.cycle.membership` as the acting user. ### 19.0.2.3.3 diff --git a/spp_programs/static/description/index.html b/spp_programs/static/description/index.html index 43af587d9..ac614eea7 100644 --- a/spp_programs/static/description/index.html +++ b/spp_programs/static/description/index.html @@ -661,9 +661,11 @@

    Changelog

    19.0.2.3.4

    • fix(security): the Tier-3 group_registry_read group can read -spp.cycle and spp.cycle.membership. The registrant form’s -entitlement lists render cycle_id, and both models were granted -only to the Tier-2 group_registry_viewer group.
    • +spp.cycle and spp.cycle.membership. Both were granted only to +the Tier-2 group_registry_viewer group, yet the registrant form +depends on both: the entitlement lists render cycle_id, and the +membership list renders latest_cycle_state, which is computed by +searching spp.cycle.membership as the acting user.
    diff --git a/spp_programs/tests/test_registry_read_access.py b/spp_programs/tests/test_registry_read_access.py index 2bc68ff3b..78a829b6b 100644 --- a/spp_programs/tests/test_registry_read_access.py +++ b/spp_programs/tests/test_registry_read_access.py @@ -9,6 +9,7 @@ Tier-3 group carries these models too. """ +from odoo import Command from odoo.tests import TransactionCase, tagged _MODELS = [ @@ -26,8 +27,8 @@ def test_tier3_registry_read_covers_registrant_form_models(self): "login": "tier3_reader_spp_programs", "email": "tier3_reader_spp_programs@example.com", "group_ids": [ - (4, self.env.ref("base.group_user").id), - (4, self.env.ref("spp_registry.group_registry_read").id), + Command.link(self.env.ref("base.group_user").id), + Command.link(self.env.ref("spp_registry.group_registry_read").id), ], } )
    Field