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
12 changes: 12 additions & 0 deletions spp_scoring_programs/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,18 @@ Dependencies
Changelog
=========

19.0.2.0.2
~~~~~~~~~~

- fix(security): stop granting ``spp_scoring.group_scoring_viewer``
direct read on ``spp.program.membership``. Because the membership
``_inherits`` ``res.partner``, that grant exposed registrant PII
(identity, contacts, IDs, bank details, relationships, other
programs/entitlements) to any scoring viewer over RPC, without a
program or registry role. Membership access is now governed solely by
``spp_programs`` ACLs, as documented; scoring viewers keep their
scoring-model/result remit and read access to ``spp.program``.

19.0.2.0.0
~~~~~~~~~~

Expand Down
2 changes: 1 addition & 1 deletion spp_scoring_programs/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{
"name": "OpenSPP Scoring Programs Bridge",
"category": "OpenSPP/Targeting",
"version": "19.0.2.0.1",
"version": "19.0.2.0.2",
"sequence": 1,
"author": "OpenSPP.org",
"website": "https://github.com/OpenSPP/OpenSPP2",
Expand Down
10 changes: 10 additions & 0 deletions spp_scoring_programs/readme/HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
### 19.0.2.0.2

- fix(security): stop granting ``spp_scoring.group_scoring_viewer`` direct read
on ``spp.program.membership``. Because the membership ``_inherits``
``res.partner``, that grant exposed registrant PII (identity, contacts, IDs,
bank details, relationships, other programs/entitlements) to any scoring
viewer over RPC, without a program or registry role. Membership access is now
governed solely by ``spp_programs`` ACLs, as documented; scoring viewers keep
their scoring-model/result remit and read access to ``spp.program``.

### 19.0.2.0.0

- Initial migration to OpenSPP2
1 change: 0 additions & 1 deletion spp_scoring_programs/security/ir.model.access.csv
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_spp_program_scoring_viewer,spp.program scoring viewer,spp_programs.model_spp_program,spp_scoring.group_scoring_viewer,1,0,0,0
access_spp_program_membership_scoring_viewer,spp.program.membership scoring viewer,spp_programs.model_spp_program_membership,spp_scoring.group_scoring_viewer,1,0,0,0
13 changes: 13 additions & 0 deletions spp_scoring_programs/static/description/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,19 @@ <h2><a class="toc-backref" href="#toc-entry-1">Changelog</a></h2>
</div>
</div>
<div class="section" id="section-1">
<h1>19.0.2.0.2</h1>
<ul class="simple">
<li>fix(security): stop granting <tt class="docutils literal">spp_scoring.group_scoring_viewer</tt>
direct read on <tt class="docutils literal">spp.program.membership</tt>. Because the membership
<tt class="docutils literal">_inherits</tt> <tt class="docutils literal">res.partner</tt>, that grant exposed registrant PII
(identity, contacts, IDs, bank details, relationships, other
programs/entitlements) to any scoring viewer over RPC, without a
program or registry role. Membership access is now governed solely by
<tt class="docutils literal">spp_programs</tt> ACLs, as documented; scoring viewers keep their
scoring-model/result remit and read access to <tt class="docutils literal">spp.program</tt>.</li>
</ul>
</div>
<div class="section" id="section-2">
<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_scoring_programs/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from . import test_scoring_programs
from . import test_scoring_viewer_access
76 changes: 76 additions & 0 deletions spp_scoring_programs/tests/test_scoring_viewer_access.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
"""Security: a scoring viewer must not get broad program-membership PII access.

``spp.program.membership`` ``_inherits`` ``res.partner``, so read access to it
exposes registrant PII (names, addresses, contacts, IDs, bank details, etc.).
The bridge module must NOT grant ``spp_scoring.group_scoring_viewer`` direct
read on the membership model — membership access is governed by ``spp_programs``
ACLs. A scoring viewer keeps its own remit (scoring models/results) and program
read; it sees memberships only if it also holds a program/registry viewer role.
"""

from odoo.exceptions import AccessError
from odoo.tests import TransactionCase, tagged


@tagged("post_install", "-at_install")
class TestScoringViewerMembershipAccess(TransactionCase):
"""A pure scoring viewer cannot read program memberships."""

@classmethod
def setUpClass(cls):
super().setUpClass()
cls.Program = cls.env["spp.program"]
cls.Membership = cls.env["spp.program.membership"]

cls.registrant = cls.env["res.partner"].create(
{
"name": "Scoring PII Test Registrant",
"given_name": "Scoring",
"family_name": "Registrant",
"is_registrant": True,
"is_group": False,
}
)
cls.program = cls.Program.create({"name": "Scoring PII Test Program"})
cls.membership = cls.Membership.create({"partner_id": cls.registrant.id, "program_id": cls.program.id})

# Internal user baseline so the AccessError is attributable to the
# missing membership ACL, not to lacking internal-user status.
base_user_group = cls.env.ref("base.group_user")
scoring_viewer_group = cls.env.ref("spp_scoring.group_scoring_viewer")
programs_viewer_group = cls.env.ref("spp_programs.group_programs_viewer")

# Pure scoring viewer — scoring remit only, no program/registry role.
cls.scoring_viewer = cls.env["res.users"].create(
{
"name": "Pure Scoring Viewer",
"login": "scoring_viewer_pii_test",
"group_ids": [(6, 0, [base_user_group.id, scoring_viewer_group.id])],
}
)
# Scoring viewer who is ALSO a program viewer — membership access comes
# from spp_programs and must be unaffected.
cls.dual_viewer = cls.env["res.users"].create(
{
"name": "Scoring + Program Viewer",
"login": "scoring_program_viewer_pii_test",
"group_ids": [(6, 0, [base_user_group.id, scoring_viewer_group.id, programs_viewer_group.id])],
}
)

def test_pure_scoring_viewer_cannot_read_membership(self):
with self.assertRaises(AccessError):
self.Membership.with_user(self.scoring_viewer).search_read(
[("id", "=", self.membership.id)], ["partner_id"]
)

def test_scoring_viewer_can_still_read_program(self):
# The spp.program read grant is retained (scoring viewers see which
# programs use a scoring model).
found = self.Program.with_user(self.scoring_viewer).search([("id", "=", self.program.id)])
self.assertIn(self.program, found)

def test_combined_viewer_can_read_membership(self):
found = self.Membership.with_user(self.dual_viewer).search([("id", "=", self.membership.id)])
self.assertIn(self.membership, found)
Loading