Skip to content

Commit f6861ca

Browse files
fix(spp_grm): scope portal users to their own grievance tickets (#380)
spp.grm.ticket granted base.group_portal read/write/create with no ir.rule targeting portal, so any portal user could read and rewrite every grievance in the system over RPC (the controller's partner scoping is presentation-only). - New portal record rule: partner_id == user.partner_id (own tickets only). - Portal ACL row reduced to read-only (1,0,0,0); submission is handled by the sudo'd portal controller, which needs no direct model write/create. - New tests/test_portal_ticket_acl.py: portal cannot read/search/write/create others' tickets; can read own.
1 parent dc486df commit f6861ca

6 files changed

Lines changed: 114 additions & 2 deletions

File tree

spp_grm/__manifest__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
{
44
"name": "OpenSPP - Grievance Redress Mechanism",
55
"summary": "Provides a centralized Grievance Redress Mechanism for receiving, tracking, and resolving beneficiary complaints and feedback. It supports multi-channel submission, manages resolution workflows through customizable stages, and links grievances directly to individual or group registrants.",
6-
"version": "19.0.2.0.1",
6+
"version": "19.0.2.0.2",
77
"sequence": 1,
88
"author": "OpenSPP.org",
99
"website": "https://github.com/OpenSPP/OpenSPP2",

spp_grm/readme/HISTORY.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
### 19.0.2.0.2
2+
3+
- fix(security): portal users can now only access their OWN grievance tickets. The
4+
``spp.grm.ticket`` portal access was read/write/create with no record rule, so any portal user
5+
could read and rewrite every grievance in the system over RPC (#380). Added a portal record rule
6+
scoping to the user's own partner and reduced the portal access-control entry to read-only
7+
(submission is handled by the sudo'd portal controller, which needs no direct model write).
8+
19
### 19.0.2.0.1
210

311
- fix(views): gate the "Helpdesk" top-level menu (`spp_grm_ticket_main_menu`) on `group_grm_viewer`. Previously the root menu had no `groups=` attribute and was visible to every logged-in user; the OP#951 menu audit requires several roles to NOT see it (Registry Viewer, Global Finance, Global Program Manager, Program Viewer/Validator/Cycle Approver, Global Registrar, CR roles, Farm User/Manager).

spp_grm/security/ir.model.access.csv

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ access_spp_grm_ticket_viewer,GRM Ticket Viewer Access,model_spp_grm_ticket,group
33
access_spp_grm_ticket_officer,GRM Ticket Officer Access,model_spp_grm_ticket,group_grm_officer,1,1,1,0
44
access_spp_grm_ticket_manager,GRM Ticket Manager Access,model_spp_grm_ticket,group_grm_manager,1,1,1,1
55
access_spp_grm_ticket_base_user,GRM Ticket Base User Access,model_spp_grm_ticket,base.group_user,1,0,0,0
6-
access_spp_grm_ticket_portal_user,GRM Ticket Portal User Access,model_spp_grm_ticket,base.group_portal,1,1,1,0
6+
access_spp_grm_ticket_portal_user,GRM Ticket Portal User Access,model_spp_grm_ticket,base.group_portal,1,0,0,0
77

88
access_spp_grm_ticket_stage_viewer,GRM Ticket Stage Viewer Access,model_spp_grm_ticket_stage,group_grm_viewer,1,0,0,0
99
access_spp_grm_ticket_stage_officer,GRM Ticket Stage Officer Access,model_spp_grm_ticket_stage,group_grm_officer,1,0,0,0

spp_grm/security/rules.xml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,4 +104,20 @@
104104
<field name="domain_force">[(1, '=', 1)]</field>
105105
<field name="groups" eval="[Command.link(ref('group_grm_manager'))]" />
106106
</record>
107+
108+
<!-- Portal users may only see their OWN grievance tickets. Without this rule
109+
the portal ACL row (read-only) is unscoped and every portal user could
110+
read every grievance in the system (#380). Read-only: portal submission
111+
is mediated by the sudo'd controller, so no direct write/create is
112+
granted; the perms here mirror the ACL as defense in depth. -->
113+
<record id="rule_spp_grm_ticket_portal" model="ir.rule">
114+
<field name="name">GRM Ticket: Portal Own Tickets Only</field>
115+
<field ref="model_spp_grm_ticket" name="model_id" />
116+
<field name="domain_force">[('partner_id', '=', user.partner_id.id)]</field>
117+
<field name="groups" eval="[Command.link(ref('base.group_portal'))]" />
118+
<field name="perm_read" eval="True" />
119+
<field name="perm_write" eval="False" />
120+
<field name="perm_create" eval="False" />
121+
<field name="perm_unlink" eval="False" />
122+
</record>
107123
</odoo>

spp_grm/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
from . import test_grm_ticket_stage
33
from . import test_res_partner
44
from . import test_grm_security
5+
from . import test_portal_ticket_acl
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
2+
"""Security: portal users must only reach their OWN grievance tickets.
3+
4+
Regression test for #380: spp.grm.ticket granted base.group_portal read/write/create
5+
with NO ir.rule targeting portal, so a portal user could read and rewrite every
6+
grievance in the system over RPC. The controller's partner_id scoping is
7+
presentation-only. Fix: a portal record rule scoping to the user's own partner, and
8+
the portal ACL row reduced to read-only (portal submission runs through the sudo'd
9+
controller, which needs no direct model write/create).
10+
"""
11+
12+
from odoo import Command
13+
from odoo.exceptions import AccessError
14+
from odoo.tests import TransactionCase, tagged
15+
16+
17+
@tagged("post_install", "-at_install")
18+
class TestPortalTicketAcl(TransactionCase):
19+
@classmethod
20+
def setUpClass(cls):
21+
super().setUpClass()
22+
Users = cls.env["res.users"]
23+
cls.portal_a = Users.create(
24+
{
25+
"name": "Portal A",
26+
"login": "grm_portal_a",
27+
"group_ids": [Command.link(cls.env.ref("base.group_portal").id)],
28+
}
29+
)
30+
cls.portal_b = Users.create(
31+
{
32+
"name": "Portal B",
33+
"login": "grm_portal_b",
34+
"group_ids": [Command.link(cls.env.ref("base.group_portal").id)],
35+
}
36+
)
37+
Ticket = cls.env["spp.grm.ticket"]
38+
cls.ticket_a = Ticket.create(
39+
{
40+
"name": "A's grievance",
41+
"description": "Private to A",
42+
"partner_id": cls.portal_a.partner_id.id,
43+
}
44+
)
45+
cls.ticket_b = Ticket.create(
46+
{
47+
"name": "B's grievance",
48+
"description": "Private to B",
49+
"partner_id": cls.portal_b.partner_id.id,
50+
}
51+
)
52+
53+
def test_portal_can_read_own_ticket(self):
54+
"""A portal user reads their own grievance (controller-created)."""
55+
own = self.ticket_a.with_user(self.portal_a)
56+
self.assertEqual(own.name, "A's grievance")
57+
58+
def test_portal_cannot_read_others_ticket(self):
59+
"""A portal user must NOT be able to read another user's grievance."""
60+
with self.assertRaises(AccessError):
61+
self.ticket_b.with_user(self.portal_a).read(["name"])
62+
63+
def test_portal_cannot_search_others_ticket(self):
64+
"""search must not surface other users' grievances to a portal user."""
65+
visible = self.env["spp.grm.ticket"].with_user(self.portal_a).search([])
66+
self.assertIn(self.ticket_a, visible)
67+
self.assertNotIn(self.ticket_b, visible)
68+
69+
def test_portal_cannot_write_any_ticket(self):
70+
"""Portal ACL is read-only: no write on own or others' tickets over RPC
71+
(edits go through the controller, not direct model writes)."""
72+
with self.assertRaises(AccessError):
73+
self.ticket_a.with_user(self.portal_a).write({"name": "tampered"})
74+
with self.assertRaises(AccessError):
75+
self.ticket_b.with_user(self.portal_a).write({"name": "hijacked"})
76+
77+
def test_portal_cannot_create_ticket_directly(self):
78+
"""Portal ACL is read-only: direct model create is denied (submission is
79+
controller-mediated via sudo)."""
80+
with self.assertRaises(AccessError):
81+
self.env["spp.grm.ticket"].with_user(self.portal_a).create(
82+
{
83+
"name": "direct",
84+
"description": "bypass controller",
85+
"partner_id": self.portal_a.partner_id.id,
86+
}
87+
)

0 commit comments

Comments
 (0)