Skip to content
Merged
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_grm_cel/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,20 @@ Dependencies
Changelog
=========

19.0.2.0.1
~~~~~~~~~~

- fix(security): restrict GRM routing and escalation rules to GRM staff.
Portal users no longer hold write/create on ``spp.grm.routing.rule``
and ``spp.grm.escalation.rule``; these models carry no record rules,
so the access-control entry was the only boundary. Because portal
grievance submission runs sudo, a portal-authored rule previously
executed its CEL condition and actions as superuser.
- fix(grm): increment the escalation counter with elevated rights,
matching the routing rule's ``match_count`` update, so a caller
without write access cannot leave an escalation applied half-way
(notification sent and case created, counter and chatter missing).

19.0.2.0.0
~~~~~~~~~~

Expand Down
2 changes: 1 addition & 1 deletion spp_grm_cel/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{
"name": "OpenSPP GRM: CEL Rules",
"summary": "CEL-based routing and escalation rules for GRM tickets",
"version": "19.0.2.0.0",
"version": "19.0.2.0.1",
"license": "LGPL-3",
"development_status": "Production/Stable",
"maintainers": ["jeremi", "gonzalesedwin1123", "emjay0921"],
Expand Down
3 changes: 2 additions & 1 deletion spp_grm_cel/models/grm_escalation_rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,8 @@ def apply_escalation(self, ticket):
self._create_case_from_ticket(ticket)

# Update escalation count
self.write({"escalation_count": self.escalation_count + 1})
# nosemgrep: semgrep.odoo-sudo-without-context -- counter update needs sudo
self.sudo().write({"escalation_count": self.escalation_count + 1})

# Post message to chatter
ticket.message_post(
Expand Down
11 changes: 11 additions & 0 deletions spp_grm_cel/readme/HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
### 19.0.2.0.1

- fix(security): restrict GRM routing and escalation rules to GRM staff. Portal users no longer
hold write/create on ``spp.grm.routing.rule`` and ``spp.grm.escalation.rule``; these models
carry no record rules, so the access-control entry was the only boundary. Because portal
grievance submission runs sudo, a portal-authored rule previously executed its CEL condition
and actions as superuser.
- fix(grm): increment the escalation counter with elevated rights, matching the routing rule's
``match_count`` update, so a caller without write access cannot leave an escalation applied
half-way (notification sent and case created, counter and chatter missing).

### 19.0.2.0.0

- Initial migration to OpenSPP2
4 changes: 2 additions & 2 deletions spp_grm_cel/security/ir.model.access.csv
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ access_spp_grm_routing_rule_viewer,GRM Routing Rule Viewer Access,model_spp_grm_
access_spp_grm_routing_rule_officer,GRM Routing Rule Officer Access,model_spp_grm_routing_rule,spp_grm.group_grm_officer,1,1,1,0
access_spp_grm_routing_rule_manager,GRM Routing Rule Manager Access,model_spp_grm_routing_rule,spp_grm.group_grm_manager,1,1,1,1
access_spp_grm_routing_rule_base_user,GRM Routing Rule Base User Access,model_spp_grm_routing_rule,base.group_user,1,0,0,0
access_spp_grm_routing_rule_portal_user,GRM Routing Rule Portal User Access,model_spp_grm_routing_rule,base.group_portal,1,1,1,0
access_spp_grm_routing_rule_portal_user,GRM Routing Rule Portal User Access,model_spp_grm_routing_rule,base.group_portal,1,0,0,0
access_spp_grm_escalation_rule_viewer,GRM Escalation Rule Viewer Access,model_spp_grm_escalation_rule,spp_grm.group_grm_viewer,1,0,0,0
access_spp_grm_escalation_rule_officer,GRM Escalation Rule Officer Access,model_spp_grm_escalation_rule,spp_grm.group_grm_officer,1,1,1,0
access_spp_grm_escalation_rule_manager,GRM Escalation Rule Manager Access,model_spp_grm_escalation_rule,spp_grm.group_grm_manager,1,1,1,1
access_spp_grm_escalation_rule_base_user,GRM Escalation Rule Base User Access,model_spp_grm_escalation_rule,base.group_user,1,0,0,0
access_spp_grm_escalation_rule_portal_user,GRM Escalation Rule Portal User Access,model_spp_grm_escalation_rule,base.group_portal,1,1,1,0
access_spp_grm_escalation_rule_portal_user,GRM Escalation Rule Portal User Access,model_spp_grm_escalation_rule,base.group_portal,1,0,0,0
15 changes: 15 additions & 0 deletions spp_grm_cel/static/description/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,21 @@ <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.1</h1>
<ul class="simple">
<li>fix(security): restrict GRM routing and escalation rules to GRM staff.
Portal users no longer hold write/create on <tt class="docutils literal">spp.grm.routing.rule</tt>
and <tt class="docutils literal">spp.grm.escalation.rule</tt>; these models carry no record rules,
so the access-control entry was the only boundary. Because portal
grievance submission runs sudo, a portal-authored rule previously
executed its CEL condition and actions as superuser.</li>
<li>fix(grm): increment the escalation counter with elevated rights,
matching the routing rule’s <tt class="docutils literal">match_count</tt> update, so a caller
without write access cannot leave an escalation applied half-way
(notification sent and case created, counter and chatter missing).</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_grm_cel/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@

from . import test_routing_rules
from . import test_escalation_rules
from . import test_rule_acl
83 changes: 83 additions & 0 deletions spp_grm_cel/tests/test_rule_acl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
"""Security: GRM automation rules must not be writable by portal users.

Regression test for "Portal users can create global GRM automation rules": the
ACL granted ``base.group_portal`` read/write/create on ``spp.grm.routing.rule``
and ``spp.grm.escalation.rule``. These are global config models (no record
rules), and routing rules run on ticket creation while escalation rules run on
stage changes and via the hourly cron over all open tickets. A portal user could
therefore plant an always-matching rule via RPC and disrupt grievance handling
globally.

Portal users must keep only READ access (rule evaluation runs as the current
user, mirroring base.group_user) — never write/create/unlink. GRM staff retain
full management.
"""

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

ROUTING_MODEL = "spp.grm.routing.rule"
ESCALATION_MODEL = "spp.grm.escalation.rule"


@tagged("post_install", "-at_install")
class TestGRMRuleAcl(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.portal_user = cls.env["res.users"].create(
{
"name": "GRM Portal User",
"login": "grm_portal_acl_test",
"group_ids": [Command.link(cls.env.ref("base.group_portal").id)],
}
)
cls.grm_manager = cls.env["res.users"].create(
{
"name": "GRM Manager",
"login": "grm_manager_acl_test",
"group_ids": [Command.link(cls.env.ref("spp_grm.group_grm_manager").id)],
}
)
# An existing rule (created as admin) to test write access against.
cls.routing_rule = cls.env[ROUTING_MODEL].create(
{"name": "Test Routing Rule", "condition_cel": "severity == 'critical'"}
)
cls.escalation_rule = cls.env[ESCALATION_MODEL].create(
{"name": "Test Escalation Rule", "condition_cel": "days_open > 3"}
)

def test_portal_user_cannot_create_routing_rule(self):
"""A portal user must NOT be able to create routing rules."""
with self.assertRaises(AccessError):
self.env[ROUTING_MODEL].with_user(self.portal_user).create({"name": "Portal Rule"})

def test_portal_user_cannot_create_escalation_rule(self):
"""A portal user must NOT be able to create escalation rules."""
with self.assertRaises(AccessError):
self.env[ESCALATION_MODEL].with_user(self.portal_user).create({"name": "Portal Rule"})

def test_portal_user_cannot_write_routing_rule(self):
"""A portal user must NOT be able to modify routing rules."""
with self.assertRaises(AccessError):
self.routing_rule.with_user(self.portal_user).write({"name": "Hijacked"})

def test_portal_user_cannot_write_escalation_rule(self):
"""A portal user must NOT be able to modify escalation rules."""
with self.assertRaises(AccessError):
self.escalation_rule.with_user(self.portal_user).write({"name": "Hijacked"})

def test_portal_user_can_read_rules(self):
"""Read access is retained so rule evaluation works as the current user
(matches base.group_user)."""
self.env[ROUTING_MODEL].with_user(self.portal_user).check_access("read")
self.env[ESCALATION_MODEL].with_user(self.portal_user).check_access("read")

def test_grm_manager_can_create_rules(self):
"""GRM staff must retain full management of both rule models."""
routing = self.env[ROUTING_MODEL].with_user(self.grm_manager).create({"name": "Manager Routing Rule"})
escalation = self.env[ESCALATION_MODEL].with_user(self.grm_manager).create({"name": "Manager Escalation Rule"})
self.assertTrue(routing.id)
self.assertTrue(escalation.id)
Loading