From e1ac2d49020207ed4c2819c36f617e1e11b7c65d Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Fri, 14 Aug 2026 21:29:26 +0800 Subject: [PATCH 1/2] security(roles): scope program/CR roles to Tier-3 registry read, drop registry-search menu (#353) Squashed net change of the #353 branch for the batch-2 staging rebase. Program Viewer and the CR requestor/validator/validator-HQ roles were granted spp_registry.group_registry_viewer (Tier-2), which exposes the Registry Search portal and app root. Narrow them to group_registry_read (Tier-3): registrant read is preserved, the registry-search menu subtree is pruned. Migrations on both spp_change_request_v2 (19.0.3.1.4) and spp_programs (19.0.2.2.2) revoke the viewer group from already-assigned users on upgrade via action_update_users(). --- spp_change_request_v2/README.rst | 33 +++------ spp_change_request_v2/__manifest__.py | 2 +- spp_change_request_v2/data/user_roles.xml | 16 ++++- .../migrations/19.0.3.1.4/post-migration.py | 50 ++++++++++++++ spp_change_request_v2/readme/HISTORY.md | 4 ++ .../static/description/index.html | 54 +++++---------- spp_change_request_v2/tests/__init__.py | 1 + .../tests/test_cr_roles_registry_scope.py | 60 +++++++++++++++++ spp_programs/README.rst | 23 +++---- spp_programs/__manifest__.py | 2 +- spp_programs/data/user_roles.xml | 11 ++- .../migrations/19.0.2.2.2/post-migration.py | 45 +++++++++++++ spp_programs/readme/HISTORY.md | 4 ++ spp_programs/static/description/index.html | 23 +++---- spp_programs/tests/__init__.py | 1 + .../test_program_viewer_registry_scope.py | 67 +++++++++++++++++++ 16 files changed, 303 insertions(+), 93 deletions(-) create mode 100644 spp_change_request_v2/migrations/19.0.3.1.4/post-migration.py create mode 100644 spp_change_request_v2/tests/test_cr_roles_registry_scope.py create mode 100644 spp_programs/migrations/19.0.2.2.2/post-migration.py create mode 100644 spp_programs/tests/test_program_viewer_registry_scope.py diff --git a/spp_change_request_v2/README.rst b/spp_change_request_v2/README.rst index bbb71b459..079ae8ca6 100644 --- a/spp_change_request_v2/README.rst +++ b/spp_change_request_v2/README.rst @@ -853,33 +853,16 @@ Before declaring a new CR type complete: Changelog ========= -19.0.3.1.3 +19.0.3.1.4 ~~~~~~~~~~ -- fix(security): add ownership and area record rules to every concrete - change-request detail model. Detail rows were reachable by any - ``group_cr_user`` regardless of who owned the parent change request, - so a requester could read or tamper with another user's detail data - over RPC. Each detail model now carries - user/validator/validator-HQ/manager rules scoped through its parent - change request, plus a global rule mirroring the parent's area filter. - ``spp.cr.detail.split_household.member`` is additionally scoped on - delete, the one detail model whose access-control entry grants - ``unlink`` to change-request users: requesters may delete member rows - only on their own requests, while validators and managers keep the - unrestricted delete their access-control entries grant. - -19.0.3.1.2 -~~~~~~~~~~ - -- fix(security): route and apply the same single field for - dynamic-approval change requests, and freeze the proposed change once - the request leaves draft. The selected field, its old/new values and - the detail pointer were writable after submission, so a requester - could re-route an approval or alter the value that had already been - approved. Note the mapped-source-field freeze applies to - ``field_mapping`` request types; ``custom``-strategy types freeze only - the routing selector. +- fix(security): scope the CR Requestor, Local Validator and HQ + Validator roles to Tier-3 registry read instead of Tier-2 registry + viewer. The viewer tier gates the Registry Search portal, a broad + registrant-PII enumeration surface these change-request roles do not + need; registrant read access is unchanged. A migration re-points the + roles and resynchronises existing users, since the role definitions + are ``noupdate``. 19.0.3.1.1 ~~~~~~~~~~ diff --git a/spp_change_request_v2/__manifest__.py b/spp_change_request_v2/__manifest__.py index 6210feb57..ea5dc8537 100644 --- a/spp_change_request_v2/__manifest__.py +++ b/spp_change_request_v2/__manifest__.py @@ -1,6 +1,6 @@ { "name": "OpenSPP Change Request V2", - "version": "19.0.3.1.3", + "version": "19.0.3.1.4", "sequence": 50, "category": "OpenSPP", "summary": "Configuration-driven change request system with UX improvements, conflict detection and duplicate prevention", diff --git a/spp_change_request_v2/data/user_roles.xml b/spp_change_request_v2/data/user_roles.xml index c8e4e7364..75bc23d54 100644 --- a/spp_change_request_v2/data/user_roles.xml +++ b/spp_change_request_v2/data/user_roles.xml @@ -5,6 +5,16 @@ Part of OpenSPP. See LICENSE file for full copyright and licensing details. User roles for Change Request module. --> + + @@ -18,7 +28,7 @@ User roles for Change Request module. eval="[ Command.link(ref('base.group_user')), Command.link(ref('group_cr_manager')), - Command.link(ref('spp_registry.group_registry_viewer')), + Command.link(ref('spp_registry.group_registry_read')), Command.link(ref('spp_hazard.group_hazard_viewer')), ]" /> @@ -35,7 +45,7 @@ User roles for Change Request module. eval="[ Command.link(ref('base.group_user')), Command.link(ref('group_cr_validator')), - Command.link(ref('spp_registry.group_registry_viewer')), + Command.link(ref('spp_registry.group_registry_read')), Command.link(ref('spp_hazard.group_hazard_viewer')), ]" /> @@ -52,7 +62,7 @@ User roles for Change Request module. eval="[ Command.link(ref('base.group_user')), Command.link(ref('group_cr_validator_hq')), - Command.link(ref('spp_registry.group_registry_viewer')), + Command.link(ref('spp_registry.group_registry_read')), Command.link(ref('spp_hazard.group_hazard_viewer')), ]" /> diff --git a/spp_change_request_v2/migrations/19.0.3.1.4/post-migration.py b/spp_change_request_v2/migrations/19.0.3.1.4/post-migration.py new file mode 100644 index 000000000..af0cbace6 --- /dev/null +++ b/spp_change_request_v2/migrations/19.0.3.1.4/post-migration.py @@ -0,0 +1,50 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +"""Swap the CR roles from Tier-2 ``group_registry_viewer`` to Tier-3 +``group_registry_read``. + +The roles' ``implied_ids`` are seeded from ``data/user_roles.xml`` with +``noupdate="1"``, so a released database (2026.07) keeps the old +``group_registry_viewer`` link on upgrade and would retain the Registry Search +portal menu. This migration unlinks the Tier-2 viewer group, links the Tier-3 +read group (same registrant read ACLs, no menu; read is also provided through +the group_cr_* chain), and re-materializes the group membership of users +already assigned each role. +""" + +import logging + +from odoo import SUPERUSER_ID, Command, api + +_logger = logging.getLogger(__name__) + +_ROLE_XMLIDS = [ + "spp_change_request_v2.global_role_cr_requestor", + "spp_change_request_v2.local_role_cr_validator", + "spp_change_request_v2.global_role_cr_validator_hq", +] + + +def migrate(cr, version): + if not version: + return + env = api.Environment(cr, SUPERUSER_ID, {}) + viewer = env.ref("spp_registry.group_registry_viewer", raise_if_not_found=False) + read = env.ref("spp_registry.group_registry_read", raise_if_not_found=False) + if not viewer or not read: + return + for xmlid in _ROLE_XMLIDS: + role = env.ref(xmlid, raise_if_not_found=False) + if not role: + continue + commands = [] + if viewer in role.implied_ids: + commands.append(Command.unlink(viewer.id)) + if read not in role.implied_ids: + commands.append(Command.link(read.id)) + if commands: + role.implied_ids = commands + role.action_update_users() + _logger.info( + "Migrated role %s: registry viewer -> registry read (re-synced users)", + xmlid, + ) diff --git a/spp_change_request_v2/readme/HISTORY.md b/spp_change_request_v2/readme/HISTORY.md index 6fa08a1a2..12a6f1f5f 100644 --- a/spp_change_request_v2/readme/HISTORY.md +++ b/spp_change_request_v2/readme/HISTORY.md @@ -1,3 +1,7 @@ +### 19.0.3.1.4 + +- fix(security): scope the CR Requestor, Local Validator and HQ Validator roles to Tier-3 registry read instead of Tier-2 registry viewer. The viewer tier gates the Registry Search portal, a broad registrant-PII enumeration surface these change-request roles do not need; registrant read access is unchanged. A migration re-points the roles and resynchronises existing users, since the role definitions are `noupdate`. + ### 19.0.3.1.3 - fix(security): add ownership and area record rules to every concrete change-request detail model. Detail rows were reachable by any `group_cr_user` regardless of who owned the parent change request, so a requester could read or tamper with another user's detail data over RPC. Each detail model now carries user/validator/validator-HQ/manager rules scoped through its parent change request, plus a global rule mirroring the parent's area filter. `spp.cr.detail.split_household.member` is additionally scoped on delete, the one detail model whose access-control entry grants `unlink` to change-request users: requesters may delete member rows only on their own requests, while validators and managers keep the unrestricted delete their access-control entries grant. diff --git a/spp_change_request_v2/static/description/index.html b/spp_change_request_v2/static/description/index.html index 56dd4e7ec..c1b5fb4eb 100644 --- a/spp_change_request_v2/static/description/index.html +++ b/spp_change_request_v2/static/description/index.html @@ -1339,36 +1339,18 @@

Changelog

-

19.0.3.1.3

+

19.0.3.1.4

    -
  • fix(security): add ownership and area record rules to every concrete -change-request detail model. Detail rows were reachable by any -group_cr_user regardless of who owned the parent change request, -so a requester could read or tamper with another user’s detail data -over RPC. Each detail model now carries -user/validator/validator-HQ/manager rules scoped through its parent -change request, plus a global rule mirroring the parent’s area filter. -spp.cr.detail.split_household.member is additionally scoped on -delete, the one detail model whose access-control entry grants -unlink to change-request users: requesters may delete member rows -only on their own requests, while validators and managers keep the -unrestricted delete their access-control entries grant.
  • +
  • fix(security): scope the CR Requestor, Local Validator and HQ +Validator roles to Tier-3 registry read instead of Tier-2 registry +viewer. The viewer tier gates the Registry Search portal, a broad +registrant-PII enumeration surface these change-request roles do not +need; registrant read access is unchanged. A migration re-points the +roles and resynchronises existing users, since the role definitions +are noupdate.
-

19.0.3.1.2

-
    -
  • fix(security): route and apply the same single field for -dynamic-approval change requests, and freeze the proposed change once -the request leaves draft. The selected field, its old/new values and -the detail pointer were writable after submission, so a requester -could re-route an approval or alter the value that had already been -approved. Note the mapped-source-field freeze applies to -field_mapping request types; custom-strategy types freeze only -the routing selector.
  • -
-
-

19.0.3.1.1

  • fix(change_request): enforce the (cr_type_id, reason) uniqueness @@ -1382,7 +1364,7 @@

    19.0.3.1.1

    applied) so the constraint applies cleanly on upgrade.
-
+

19.0.3.1.0

  • revert(change_request): restore the create-a-new-individual Add @@ -1400,7 +1382,7 @@

    19.0.3.1.0

    not restored here; reinstate separately if needed.
-
+

19.0.3.0.0

  • feat(change_request): redesign the group/membership CR flows (#242) — @@ -1422,7 +1404,7 @@

    19.0.3.0.0

    must adapt (see #1133).
-
+

19.0.2.0.8

  • fix(views): disable inline creation of CR document types on the Change @@ -1433,7 +1415,7 @@

    19.0.2.0.8

    Documents” modal (missing Name field) that blocked saving (#1125)
-
+

19.0.2.0.7

  • fix(security): align CR Requestor / CR Local Validator / CR HQ @@ -1445,7 +1427,7 @@

    19.0.2.0.7

    dependencies.
-
+

19.0.2.0.6

  • fix(views): route post-submit CRs (pending / approved / applied / @@ -1460,7 +1442,7 @@

    19.0.2.0.6

    list so row-click goes through the stage router.
-
+

19.0.2.0.5

  • fix(security): add a global ir.rule on spp.change.request that @@ -1473,27 +1455,27 @@

    19.0.2.0.5

    roles).
-
+

19.0.2.0.3

  • fix: add HTML escaping to all computed Html fields with sanitize=False to prevent stored XSS (#50)
-
+

19.0.2.0.2

  • fix: fix batch approval wizard line deletion (#130)
-
+

19.0.2.0.1

  • fix: skip field types before getattr and isolate detail prefetch (#129)
-
+

19.0.2.0.0

  • Initial migration to OpenSPP2
  • diff --git a/spp_change_request_v2/tests/__init__.py b/spp_change_request_v2/tests/__init__.py index 193929816..f85ca3317 100644 --- a/spp_change_request_v2/tests/__init__.py +++ b/spp_change_request_v2/tests/__init__.py @@ -27,3 +27,4 @@ from . import test_wizard_html_escaping from . import test_reason_document_constraint from . import test_detail_record_rules +from . import test_cr_roles_registry_scope diff --git a/spp_change_request_v2/tests/test_cr_roles_registry_scope.py b/spp_change_request_v2/tests/test_cr_roles_registry_scope.py new file mode 100644 index 000000000..43761489e --- /dev/null +++ b/spp_change_request_v2/tests/test_cr_roles_registry_scope.py @@ -0,0 +1,60 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +"""CR roles must read registrant data (a change request is about a registrant) +but must NOT carry the Tier-2 ``group_registry_viewer`` group, which gates the +standalone Registry Search portal menu — an over-broad registrant PII +enumeration surface. Registrant read is preserved via the Tier-3 +``group_registry_read`` group (granted through their ``group_cr_*`` chain and +the explicit role link). +""" + +from odoo.tests import TransactionCase, tagged + +_CR_ROLE_XMLIDS = [ + "spp_change_request_v2.global_role_cr_requestor", + "spp_change_request_v2.local_role_cr_validator", + "spp_change_request_v2.global_role_cr_validator_hq", +] + + +@tagged("post_install", "-at_install") +class TestCRRolesRegistryScope(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.registrant = cls.env["res.partner"].create( + {"name": "CR Registrant", "is_registrant": True, "is_group": False} + ) + cls.reg_id = cls.env["spp.registry.id"].create( + { + "partner_id": cls.registrant.id, + "id_type_id": cls.env.ref("spp_vocabulary.code_id_type_national_id").id, + "value": "CR-123", + } + ) + cls.phone = cls.env["spp.phone.number"].create({"partner_id": cls.registrant.id, "phone_no": "09180000000"}) + + def _user_with_role(self, role_xmlid, login): + user = self.env["res.users"].create({"name": login, "login": login, "email": f"{login}@example.com"}) + self.env["res.users.role.line"].create({"user_id": user.id, "role_id": self.env.ref(role_xmlid).id}) + user.set_groups_from_roles() + return user + + def test_cr_roles_lack_tier2_registry_viewer(self): + for xmlid in _CR_ROLE_XMLIDS: + user = self._user_with_role(xmlid, f"crscope_{xmlid.split('.')[-1]}") + self.assertFalse( + user.has_group("spp_registry.group_registry_viewer"), + f"{xmlid} must not carry the Tier-2 registry viewer group (it gates the registry search portal menu)", + ) + + def test_cr_roles_keep_registrant_read(self): + for xmlid in _CR_ROLE_XMLIDS: + user = self._user_with_role(xmlid, f"crread_{xmlid.split('.')[-1]}") + self.assertTrue( + user.has_group("spp_registry.group_registry_read"), + f"{xmlid} must keep Tier-3 registry read", + ) + # Functional read of the sensitive PII models as the role user. + self.registrant.with_user(user).read(["name"]) + self.reg_id.with_user(user).read(["value"]) + self.phone.with_user(user).read(["phone_no"]) diff --git a/spp_programs/README.rst b/spp_programs/README.rst index 5860f62a3..116bfa98d 100644 --- a/spp_programs/README.rst +++ b/spp_programs/README.rst @@ -254,21 +254,18 @@ Dependencies Changelog ========= -19.0.2.2.1 +19.0.2.2.2 ~~~~~~~~~~ -- fix(security): make the async operation lock a server-side boundary. - The Force Unlock buttons were gated to ``base.group_system`` in the - views, but ``action_force_unlock`` on ``spp.cycle`` / ``spp.program`` - — and direct writes to the ``is_locked`` / ``locked_reason`` fields — - had no server-side check, so any role holding write access (program - officers, managers, cycle approvers) could clear an active operation - lock via RPC while async entitlement / payment / eligibility jobs were - still running. Direct writes to the lock fields now require - ``base.group_system`` (via a ``write()`` guard), the manual - ``action_force_unlock`` override requires the same, and the async - pipeline manages the lock through ``sudo()`` helpers so legitimate - acquire/release from the initiating user keeps working. +- fix(security): the Program Viewer role no longer carries the Tier-2 + ``spp_registry.group_registry_viewer`` group, which gates the + standalone Registry Search portal menu and exposed a broad + registrant-PII enumeration surface to a read-only program role. It now + uses the Tier-3 ``spp_registry.group_registry_read`` group instead, + preserving the registrant read needed for program cross-references + (same read ACLs, defined in ``spp_base_common``) without the Registry + app menu. Includes a migration that re-points the role and re-syncs + already-assigned users on upgrade. 19.0.2.1.3 ~~~~~~~~~~ diff --git a/spp_programs/__manifest__.py b/spp_programs/__manifest__.py index 48590c716..f7c9e330b 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.2", "sequence": 1, "author": "OpenSPP.org", "website": "https://github.com/OpenSPP/OpenSPP2", diff --git a/spp_programs/data/user_roles.xml b/spp_programs/data/user_roles.xml index 62b2275f6..95b6d2642 100644 --- a/spp_programs/data/user_roles.xml +++ b/spp_programs/data/user_roles.xml @@ -7,12 +7,21 @@ Read-only access to program, cycle, and entitlement records. + registry read (re-synced users)", + xmlid, + ) diff --git a/spp_programs/readme/HISTORY.md b/spp_programs/readme/HISTORY.md index 1b094a3f1..962e2f55f 100644 --- a/spp_programs/readme/HISTORY.md +++ b/spp_programs/readme/HISTORY.md @@ -1,3 +1,7 @@ +### 19.0.2.2.2 + +- fix(security): the Program Viewer role no longer carries the Tier-2 `spp_registry.group_registry_viewer` group, which gates the standalone Registry Search portal menu and exposed a broad registrant-PII enumeration surface to a read-only program role. It now uses the Tier-3 `spp_registry.group_registry_read` group instead, preserving the registrant read needed for program cross-references (same read ACLs, defined in `spp_base_common`) without the Registry app menu. Includes a migration that re-points the role and re-syncs already-assigned users on upgrade. + ### 19.0.2.2.1 - fix(security): make the async operation lock a server-side boundary. The diff --git a/spp_programs/static/description/index.html b/spp_programs/static/description/index.html index b18978828..dc5dd2352 100644 --- a/spp_programs/static/description/index.html +++ b/spp_programs/static/description/index.html @@ -658,20 +658,17 @@

    Changelog

-

19.0.2.2.1

+

19.0.2.2.2

    -
  • fix(security): make the async operation lock a server-side boundary. -The Force Unlock buttons were gated to base.group_system in the -views, but action_force_unlock on spp.cycle / spp.program -— and direct writes to the is_locked / locked_reason fields — -had no server-side check, so any role holding write access (program -officers, managers, cycle approvers) could clear an active operation -lock via RPC while async entitlement / payment / eligibility jobs were -still running. Direct writes to the lock fields now require -base.group_system (via a write() guard), the manual -action_force_unlock override requires the same, and the async -pipeline manages the lock through sudo() helpers so legitimate -acquire/release from the initiating user keeps working.
  • +
  • fix(security): the Program Viewer role no longer carries the Tier-2 +spp_registry.group_registry_viewer group, which gates the +standalone Registry Search portal menu and exposed a broad +registrant-PII enumeration surface to a read-only program role. It now +uses the Tier-3 spp_registry.group_registry_read group instead, +preserving the registrant read needed for program cross-references +(same read ACLs, defined in spp_base_common) without the Registry +app menu. Includes a migration that re-points the role and re-syncs +already-assigned users on upgrade.
diff --git a/spp_programs/tests/__init__.py b/spp_programs/tests/__init__.py index 24988efbe..e5a31c352 100644 --- a/spp_programs/tests/__init__.py +++ b/spp_programs/tests/__init__.py @@ -44,3 +44,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_program_viewer_registry_scope diff --git a/spp_programs/tests/test_program_viewer_registry_scope.py b/spp_programs/tests/test_program_viewer_registry_scope.py new file mode 100644 index 000000000..708eb9f08 --- /dev/null +++ b/spp_programs/tests/test_program_viewer_registry_scope.py @@ -0,0 +1,67 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +"""The Program Viewer role must be able to read registrant data for program +cross-references, but must NOT carry the Tier-2 ``group_registry_viewer`` +group, which gates the standalone Registry Search portal menu +(``spp_registry_search.menu_registry_search``) — an over-broad registrant PII +enumeration surface for a read-only program role. + +The role is switched to the Tier-3 ``group_registry_read`` technical group, +which grants the same registrant read ACLs (defined in ``spp_base_common``) +without the Registry app menu. +""" + +from odoo.tests import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestProgramViewerRegistryScope(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.user = cls.env["res.users"].create( + { + "name": "Program Viewer Test", + "login": "program_viewer_scope_test", + "email": "pv_scope@example.com", + } + ) + cls.env["res.users.role.line"].create( + { + "user_id": cls.user.id, + "role_id": cls.env.ref("spp_programs.global_role_program_viewer").id, + } + ) + cls.user.set_groups_from_roles() + + # A registrant with an ID number and phone (the sensitive PII models). + cls.registrant = cls.env["res.partner"].create( + {"name": "PV Registrant", "is_registrant": True, "is_group": False} + ) + cls.reg_id = cls.env["spp.registry.id"].create( + { + "partner_id": cls.registrant.id, + "id_type_id": cls.env.ref("spp_vocabulary.code_id_type_national_id").id, + "value": "PV-123", + } + ) + cls.phone = cls.env["spp.phone.number"].create({"partner_id": cls.registrant.id, "phone_no": "09170000000"}) + + def test_program_viewer_lacks_tier2_registry_viewer(self): + """The role must not carry group_registry_viewer (gates the Registry + Search portal menu).""" + self.assertFalse( + self.user.has_group("spp_registry.group_registry_viewer"), + "Program Viewer must not have the Tier-2 registry viewer group (it gates the registry search portal menu)", + ) + + def test_program_viewer_keeps_registrant_read(self): + """Registrant read must be preserved via Tier-3 group_registry_read.""" + self.assertTrue(self.user.has_group("spp_registry.group_registry_read")) + # Functional read of the sensitive PII models as the role user. + self.registrant.with_user(self.user).read(["name"]) + self.reg_id.with_user(self.user).read(["value"]) + self.phone.with_user(self.user).read(["phone_no"]) + + def test_program_viewer_keeps_program_data_read(self): + """The role must still read program/cycle data (from group_programs_viewer).""" + self.assertTrue(self.user.has_group("spp_programs.group_programs_viewer")) From acdba200b560b8561cbf6d10cf057afc9abce82f Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Fri, 14 Aug 2026 21:42:59 +0800 Subject: [PATCH 2/2] docs: regenerate CR + programs READMEs from fragments (#353) Applied verbatim from CI's pinned oca-gen output (run 31805166557). --- spp_change_request_v2/README.rst | 28 +++++++++++ .../static/description/index.html | 50 +++++++++++++++---- spp_programs/README.rst | 16 ++++++ spp_programs/static/description/index.html | 45 +++++++++++------ 4 files changed, 115 insertions(+), 24 deletions(-) diff --git a/spp_change_request_v2/README.rst b/spp_change_request_v2/README.rst index 079ae8ca6..6830899f3 100644 --- a/spp_change_request_v2/README.rst +++ b/spp_change_request_v2/README.rst @@ -864,6 +864,34 @@ Changelog roles and resynchronises existing users, since the role definitions are ``noupdate``. +19.0.3.1.3 +~~~~~~~~~~ + +- fix(security): add ownership and area record rules to every concrete + change-request detail model. Detail rows were reachable by any + ``group_cr_user`` regardless of who owned the parent change request, + so a requester could read or tamper with another user's detail data + over RPC. Each detail model now carries + user/validator/validator-HQ/manager rules scoped through its parent + change request, plus a global rule mirroring the parent's area filter. + ``spp.cr.detail.split_household.member`` is additionally scoped on + delete, the one detail model whose access-control entry grants + ``unlink`` to change-request users: requesters may delete member rows + only on their own requests, while validators and managers keep the + unrestricted delete their access-control entries grant. + +19.0.3.1.2 +~~~~~~~~~~ + +- fix(security): route and apply the same single field for + dynamic-approval change requests, and freeze the proposed change once + the request leaves draft. The selected field, its old/new values and + the detail pointer were writable after submission, so a requester + could re-route an approval or alter the value that had already been + approved. Note the mapped-source-field freeze applies to + ``field_mapping`` request types; ``custom``-strategy types freeze only + the routing selector. + 19.0.3.1.1 ~~~~~~~~~~ diff --git a/spp_change_request_v2/static/description/index.html b/spp_change_request_v2/static/description/index.html index c1b5fb4eb..5b03c0fbf 100644 --- a/spp_change_request_v2/static/description/index.html +++ b/spp_change_request_v2/static/description/index.html @@ -1351,6 +1351,36 @@

19.0.3.1.4

+

19.0.3.1.3

+
    +
  • fix(security): add ownership and area record rules to every concrete +change-request detail model. Detail rows were reachable by any +group_cr_user regardless of who owned the parent change request, +so a requester could read or tamper with another user’s detail data +over RPC. Each detail model now carries +user/validator/validator-HQ/manager rules scoped through its parent +change request, plus a global rule mirroring the parent’s area filter. +spp.cr.detail.split_household.member is additionally scoped on +delete, the one detail model whose access-control entry grants +unlink to change-request users: requesters may delete member rows +only on their own requests, while validators and managers keep the +unrestricted delete their access-control entries grant.
  • +
+
+
+

19.0.3.1.2

+
    +
  • fix(security): route and apply the same single field for +dynamic-approval change requests, and freeze the proposed change once +the request leaves draft. The selected field, its old/new values and +the detail pointer were writable after submission, so a requester +could re-route an approval or alter the value that had already been +approved. Note the mapped-source-field freeze applies to +field_mapping request types; custom-strategy types freeze only +the routing selector.
  • +
+
+

19.0.3.1.1

  • fix(change_request): enforce the (cr_type_id, reason) uniqueness @@ -1364,7 +1394,7 @@

    19.0.3.1.1

    applied) so the constraint applies cleanly on upgrade.
-
+

19.0.3.1.0

  • revert(change_request): restore the create-a-new-individual Add @@ -1382,7 +1412,7 @@

    19.0.3.1.0

    not restored here; reinstate separately if needed.
-
+

19.0.3.0.0

  • feat(change_request): redesign the group/membership CR flows (#242) — @@ -1404,7 +1434,7 @@

    19.0.3.0.0

    must adapt (see #1133).
-
+

19.0.2.0.8

  • fix(views): disable inline creation of CR document types on the Change @@ -1415,7 +1445,7 @@

    19.0.2.0.8

    Documents” modal (missing Name field) that blocked saving (#1125)
-
+

19.0.2.0.7

  • fix(security): align CR Requestor / CR Local Validator / CR HQ @@ -1427,7 +1457,7 @@

    19.0.2.0.7

    dependencies.
-
+

19.0.2.0.6

  • fix(views): route post-submit CRs (pending / approved / applied / @@ -1442,7 +1472,7 @@

    19.0.2.0.6

    list so row-click goes through the stage router.
-
+

19.0.2.0.5

  • fix(security): add a global ir.rule on spp.change.request that @@ -1455,27 +1485,27 @@

    19.0.2.0.5

    roles).
-
+

19.0.2.0.3

  • fix: add HTML escaping to all computed Html fields with sanitize=False to prevent stored XSS (#50)
-
+

19.0.2.0.2

  • fix: fix batch approval wizard line deletion (#130)
-
+

19.0.2.0.1

  • fix: skip field types before getattr and isolate detail prefetch (#129)
-
+

19.0.2.0.0

  • Initial migration to OpenSPP2
  • diff --git a/spp_programs/README.rst b/spp_programs/README.rst index 116bfa98d..eb2737a66 100644 --- a/spp_programs/README.rst +++ b/spp_programs/README.rst @@ -267,6 +267,22 @@ Changelog app menu. Includes a migration that re-points the role and re-syncs already-assigned users on upgrade. +19.0.2.2.1 +~~~~~~~~~~ + +- fix(security): make the async operation lock a server-side boundary. + The Force Unlock buttons were gated to ``base.group_system`` in the + views, but ``action_force_unlock`` on ``spp.cycle`` / ``spp.program`` + — and direct writes to the ``is_locked`` / ``locked_reason`` fields — + had no server-side check, so any role holding write access (program + officers, managers, cycle approvers) could clear an active operation + lock via RPC while async entitlement / payment / eligibility jobs were + still running. Direct writes to the lock fields now require + ``base.group_system`` (via a ``write()`` guard), the manual + ``action_force_unlock`` override requires the same, and the async + pipeline manages the lock through ``sudo()`` helpers so legitimate + acquire/release from the initiating user keeps working. + 19.0.2.1.3 ~~~~~~~~~~ diff --git a/spp_programs/static/description/index.html b/spp_programs/static/description/index.html index dc5dd2352..bfa512194 100644 --- a/spp_programs/static/description/index.html +++ b/spp_programs/static/description/index.html @@ -672,6 +672,23 @@

    19.0.2.2.2

+

19.0.2.2.1

+
    +
  • fix(security): make the async operation lock a server-side boundary. +The Force Unlock buttons were gated to base.group_system in the +views, but action_force_unlock on spp.cycle / spp.program +— and direct writes to the is_locked / locked_reason fields — +had no server-side check, so any role holding write access (program +officers, managers, cycle approvers) could clear an active operation +lock via RPC while async entitlement / payment / eligibility jobs were +still running. Direct writes to the lock fields now require +base.group_system (via a write() guard), the manual +action_force_unlock override requires the same, and the async +pipeline manages the lock through sudo() helpers so legitimate +acquire/release from the initiating user keeps working.
  • +
+
+

19.0.2.1.3

  • fix(security): align Program Viewer / Validator / Cycle Approver roles @@ -690,7 +707,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 @@ -704,7 +721,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 @@ -713,7 +730,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 @@ -724,7 +741,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, @@ -737,7 +754,7 @@

    19.0.2.0.10

    submission on double-click
-
+

19.0.2.0.9

  • Add context flags (skip_registrant_statistics, @@ -750,7 +767,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 @@ -761,7 +778,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 @@ -770,7 +787,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)
  • @@ -779,34 +796,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 @@ -815,7 +832,7 @@

    19.0.2.0.1

    constraint creation
-
+

19.0.2.0.0

  • Initial migration to OpenSPP2