|
| 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