Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions spp_change_request_v2/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -853,6 +853,20 @@ Before declaring a new CR type complete:
Changelog
=========

19.0.3.0.3
~~~~~~~~~~

- fix(security): the CR Requestor, CR Local Validator, and CR HQ
Validator roles no longer carry 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. They now use the Tier-3
``spp_registry.group_registry_read`` group instead, preserving the
registrant read a change request needs (same read ACLs, also provided
through the ``group_cr_*`` chain) without the Registry app menu.
Includes a migration that re-points the roles and re-syncs
already-assigned users on upgrade.

19.0.3.0.0
~~~~~~~~~~

Expand Down
2 changes: 1 addition & 1 deletion spp_change_request_v2/__manifest__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "OpenSPP Change Request V2",
"version": "19.0.3.0.0",
"version": "19.0.3.0.3",
"sequence": 50,
"category": "OpenSPP",
"summary": "Configuration-driven change request system with UX improvements, conflict detection and duplicate prevention",
Expand Down
16 changes: 13 additions & 3 deletions spp_change_request_v2/data/user_roles.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ Part of OpenSPP. See LICENSE file for full copyright and licensing details.
User roles for Change Request module.
-->
<odoo noupdate="1">
<!--
Registry: these CR roles use Tier-3 `group_registry_read` (ACL-only),
not Tier-2 `group_registry_viewer`. Both grant the same registrant read
ACLs (defined in spp_base_common), but Tier-2 also gates the standalone
Registry Search portal menu — an over-broad registrant PII enumeration
surface. CR work needs to read the registrant a request is about, not
to browse the whole registry via the search portal. (Read is also
provided transitively through the group_cr_* chain.)
-->

<!-- CR Requestor Role -->
<!-- For users who can create and submit change requests -->
<record id="global_role_cr_requestor" model="res.users.role">
Expand All @@ -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')),
]"
/>
Expand All @@ -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')),
]"
/>
Expand All @@ -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')),
]"
/>
Expand Down
50 changes: 50 additions & 0 deletions spp_change_request_v2/migrations/19.0.3.0.3/post-migration.py
Original file line number Diff line number Diff line change
@@ -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,
)
4 changes: 4 additions & 0 deletions spp_change_request_v2/readme/HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
### 19.0.3.0.3

- fix(security): the CR Requestor, CR Local Validator, and CR HQ Validator roles no longer carry 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. They now use the Tier-3 `spp_registry.group_registry_read` group instead, preserving the registrant read a change request needs (same read ACLs, also provided through the `group_cr_*` chain) without the Registry app menu. Includes a migration that re-points the roles and re-syncs already-assigned users on upgrade.

### 19.0.3.0.0

- feat(change_request): redesign the group/membership CR flows (#242) — Create Group (#876), Add Member now searches an existing member (#871), Remove Member first-page/review cleanup (#872), Change Head of Household via a per-member role table (#873), and Split Household as a relational member move with single-head validation (#877). Review pages render the real data as tables / detail sections.
Expand Down
31 changes: 23 additions & 8 deletions spp_change_request_v2/static/description/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1339,6 +1339,21 @@ <h2>Changelog</h2>
</div>
</div>
<div class="section" id="section-1">
<h1>19.0.3.0.3</h1>
<ul class="simple">
<li>fix(security): the CR Requestor, CR Local Validator, and CR HQ
Validator roles no longer carry the Tier-2
<tt class="docutils literal">spp_registry.group_registry_viewer</tt> group, which gates the
standalone Registry Search portal menu and exposed a broad
registrant-PII enumeration surface. They now use the Tier-3
<tt class="docutils literal">spp_registry.group_registry_read</tt> group instead, preserving the
registrant read a change request needs (same read ACLs, also provided
through the <tt class="docutils literal">group_cr_*</tt> chain) without the Registry app menu.
Includes a migration that re-points the roles and re-syncs
already-assigned users on upgrade.</li>
</ul>
</div>
<div class="section" id="section-2">
<h1>19.0.3.0.0</h1>
<ul class="simple">
<li>feat(change_request): redesign the group/membership CR flows (#242) —
Expand All @@ -1360,7 +1375,7 @@ <h1>19.0.3.0.0</h1>
must adapt (see #1133).</li>
</ul>
</div>
<div class="section" id="section-2">
<div class="section" id="section-3">
<h1>19.0.2.0.8</h1>
<ul class="simple">
<li>fix(views): disable inline creation of CR document types on the Change
Expand All @@ -1371,7 +1386,7 @@ <h1>19.0.2.0.8</h1>
Documents” modal (missing Name field) that blocked saving (#1125)</li>
</ul>
</div>
<div class="section" id="section-3">
<div class="section" id="section-4">
<h1>19.0.2.0.7</h1>
<ul class="simple">
<li>fix(security): align CR Requestor / CR Local Validator / CR HQ
Expand All @@ -1383,7 +1398,7 @@ <h1>19.0.2.0.7</h1>
dependencies.</li>
</ul>
</div>
<div class="section" id="section-4">
<div class="section" id="section-5">
<h1>19.0.2.0.6</h1>
<ul class="simple">
<li>fix(views): route post-submit CRs (pending / approved / applied /
Expand All @@ -1398,7 +1413,7 @@ <h1>19.0.2.0.6</h1>
list so row-click goes through the stage router.</li>
</ul>
</div>
<div class="section" id="section-5">
<div class="section" id="section-6">
<h1>19.0.2.0.5</h1>
<ul class="simple">
<li>fix(security): add a global <tt class="docutils literal">ir.rule</tt> on <tt class="docutils literal">spp.change.request</tt> that
Expand All @@ -1411,27 +1426,27 @@ <h1>19.0.2.0.5</h1>
roles).</li>
</ul>
</div>
<div class="section" id="section-6">
<div class="section" id="section-7">
<h1>19.0.2.0.3</h1>
<ul class="simple">
<li>fix: add HTML escaping to all computed Html fields with
<tt class="docutils literal">sanitize=False</tt> to prevent stored XSS (#50)</li>
</ul>
</div>
<div class="section" id="section-7">
<div class="section" id="section-8">
<h1>19.0.2.0.2</h1>
<ul class="simple">
<li>fix: fix batch approval wizard line deletion (#130)</li>
</ul>
</div>
<div class="section" id="section-8">
<div class="section" id="section-9">
<h1>19.0.2.0.1</h1>
<ul class="simple">
<li>fix: skip field types before getattr and isolate detail prefetch
(#129)</li>
</ul>
</div>
<div class="section" id="section-9">
<div class="section" id="section-10">
<h1>19.0.2.0.0</h1>
<ul class="simple">
<li>Initial migration to OpenSPP2</li>
Expand Down
1 change: 1 addition & 0 deletions spp_change_request_v2/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@
from . import test_conflict_dynamic_approval
from . import test_html_escaping
from . import test_wizard_html_escaping
from . import test_cr_roles_registry_scope
60 changes: 60 additions & 0 deletions spp_change_request_v2/tests/test_cr_roles_registry_scope.py
Original file line number Diff line number Diff line change
@@ -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"])
13 changes: 13 additions & 0 deletions spp_programs/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,19 @@ Dependencies
Changelog
=========

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.1.3
~~~~~~~~~~

Expand Down
2 changes: 1 addition & 1 deletion spp_programs/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.0",
"version": "19.0.2.2.2",
"sequence": 1,
"author": "OpenSPP.org",
"website": "https://github.com/OpenSPP/OpenSPP2",
Expand Down
11 changes: 10 additions & 1 deletion spp_programs/data/user_roles.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,21 @@
<field
name="comment"
>Read-only access to program, cycle, and entitlement records.</field>
<!--
Registry: use Tier-3 `group_registry_read` (ACL-only) rather than
Tier-2 `group_registry_viewer`. Both grant the same registrant read
ACLs (defined in spp_base_common), but Tier-2 also gates the
standalone Registry Search portal menu — an over-broad PII
enumeration surface for a read-only program role. Tier-3 preserves
the registrant read needed for program cross-references without the
Registry app menu.
-->
<field
name="implied_ids"
eval="[
Command.link(ref('base.group_user')),
Command.link(ref('group_programs_viewer')),
Command.link(ref('spp_registry.group_registry_viewer')),
Command.link(ref('spp_registry.group_registry_read')),
Command.link(ref('spp_approval.group_approval_viewer')),
Command.link(ref('spp_hazard.group_hazard_viewer')),
Command.link(ref('spp_gis_report.group_gis_report_user')),
Expand Down
45 changes: 45 additions & 0 deletions spp_programs/migrations/19.0.2.2.2/post-migration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
"""Swap the Program Viewer role from Tier-2 ``group_registry_viewer`` to Tier-3
``group_registry_read``.

The role's ``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), and re-materializes the group
membership of users already assigned the role.
"""

import logging

from odoo import SUPERUSER_ID, Command, api

_logger = logging.getLogger(__name__)

_ROLE_XMLIDS = ["spp_programs.global_role_program_viewer"]


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,
)
4 changes: 4 additions & 0 deletions spp_programs/readme/HISTORY.md
Original file line number Diff line number Diff line change
@@ -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.1.3

- fix(security): align Program Viewer / Validator / Cycle Approver roles with the OP#951 menu audit — Program Viewer additionally gets `group_registry_viewer` + `group_approval_viewer` (read-only Registry + Approvals access); all three program roles get `group_hazard_viewer` + `group_gis_report_user` so they retain Hazard / GIS Reports visibility once those menu roots are gated. Adds `spp_hazard` and `spp_gis_report` to module dependencies.
Expand Down
Loading
Loading