fix(fusion_helpdesk): define missing _norm_email helper (NameError on every inbox endpoint)
The IDOR-hardening commit (fb345be3) referenced _norm_email in submit()
and _identity() but never defined or imported it, so every inbox endpoint
(my_tickets, ticket_detail, ticket_reply, unread_count) and the New-ticket
submit path raised NameError at runtime — surfaced as a JSON-RPC error in
the dialog. The smoke test missed it because it replicated build_scope_domain
directly instead of calling the controller.
Add _norm_email(*candidates) to utils.py (returns the first candidate that
email_normalize accepts, else '' — rejecting self-set wildcards like '%' so
the inbox scope key can't be widened), import it in main.py, drop the now-unused
email_normalize import there, and add unit tests + a guard that the controller
namespace resolves the name.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
{
|
{
|
||||||
'name': 'Fusion Helpdesk Reporter',
|
'name': 'Fusion Helpdesk Reporter',
|
||||||
'version': '19.0.1.4.0',
|
'version': '19.0.1.4.1',
|
||||||
'category': 'Productivity',
|
'category': 'Productivity',
|
||||||
'summary': 'One-click in-app bug reporting & feature requesting — '
|
'summary': 'One-click in-app bug reporting & feature requesting — '
|
||||||
'auto-creates a helpdesk.ticket on a central Odoo Helpdesk.',
|
'auto-creates a helpdesk.ticket on a central Odoo Helpdesk.',
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ from urllib.parse import urljoin, urlparse
|
|||||||
from odoo import _, http
|
from odoo import _, http
|
||||||
from odoo.exceptions import UserError
|
from odoo.exceptions import UserError
|
||||||
from odoo.http import request
|
from odoo.http import request
|
||||||
from odoo.tools import email_normalize
|
|
||||||
|
|
||||||
from odoo.addons.fusion_helpdesk.utils import (
|
from odoo.addons.fusion_helpdesk.utils import (
|
||||||
build_ticket_vals,
|
build_ticket_vals,
|
||||||
@@ -30,6 +29,7 @@ from odoo.addons.fusion_helpdesk.utils import (
|
|||||||
is_public_message,
|
is_public_message,
|
||||||
compute_unread_count,
|
compute_unread_count,
|
||||||
escape_like,
|
escape_like,
|
||||||
|
_norm_email,
|
||||||
)
|
)
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from odoo.addons.fusion_helpdesk.utils import (
|
|||||||
build_scope_domain,
|
build_scope_domain,
|
||||||
is_public_message,
|
is_public_message,
|
||||||
compute_unread_count,
|
compute_unread_count,
|
||||||
|
_norm_email,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -103,3 +104,28 @@ class TestMessageFilterAndUnread(TransactionCase):
|
|||||||
def test_unread_count_unseen_ticket_counts(self):
|
def test_unread_count_unseen_ticket_counts(self):
|
||||||
tickets = [{'id': 9, 'last_support_msg_id': 4}]
|
tickets = [{'id': 9, 'last_support_msg_id': 4}]
|
||||||
self.assertEqual(compute_unread_count(tickets, {}), 1)
|
self.assertEqual(compute_unread_count(tickets, {}), 1)
|
||||||
|
|
||||||
|
|
||||||
|
@tagged('post_install', '-at_install', 'fusion_helpdesk')
|
||||||
|
class TestNormEmail(TransactionCase):
|
||||||
|
|
||||||
|
def test_valid_email_is_normalised_lowercase(self):
|
||||||
|
self.assertEqual(_norm_email('John@Entech.COM'), 'john@entech.com')
|
||||||
|
|
||||||
|
def test_first_valid_candidate_wins(self):
|
||||||
|
# confirmed reply email empty -> fall back to the next valid one
|
||||||
|
self.assertEqual(_norm_email('', 'not an email', 'jane@x.com'), 'jane@x.com')
|
||||||
|
|
||||||
|
def test_wildcard_is_rejected(self):
|
||||||
|
# IDOR guard: a self-set '%' must not survive as a scope key
|
||||||
|
self.assertEqual(_norm_email('%'), '')
|
||||||
|
|
||||||
|
def test_non_email_login_falls_through_to_empty(self):
|
||||||
|
self.assertEqual(_norm_email('admin', 'also-not-email', ''), '')
|
||||||
|
|
||||||
|
def test_controller_namespace_resolves_norm_email(self):
|
||||||
|
# Regression: _norm_email was called in controllers/main.py
|
||||||
|
# (submit + _identity) but never imported/defined -> NameError on
|
||||||
|
# every inbox endpoint. Guard that the name is resolvable there.
|
||||||
|
from odoo.addons.fusion_helpdesk.controllers import main
|
||||||
|
self.assertTrue(hasattr(main, '_norm_email'))
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ here is unit-testable in isolation, which is what lets us validate the
|
|||||||
identity keystone, the server-side scoping boundary, the public-message
|
identity keystone, the server-side scoping boundary, the public-message
|
||||||
filter and the unread maths without a live central Odoo to talk to.
|
filter and the unread maths without a live central Odoo to talk to.
|
||||||
"""
|
"""
|
||||||
|
from odoo.tools import email_normalize
|
||||||
|
|
||||||
# Sentinel used so a missing label/email can never widen a domain to
|
# Sentinel used so a missing label/email can never widen a domain to
|
||||||
# "match everything". An empty string in `=`/`=ilike` would match rows
|
# "match everything". An empty string in `=`/`=ilike` would match rows
|
||||||
@@ -29,6 +30,24 @@ def escape_like(value):
|
|||||||
return (value or '').replace('\\', '\\\\').replace('%', '\\%').replace('_', '\\_')
|
return (value or '').replace('\\', '\\\\').replace('%', '\\%').replace('_', '\\_')
|
||||||
|
|
||||||
|
|
||||||
|
def _norm_email(*candidates):
|
||||||
|
"""Return the first candidate that normalises to a valid email, else ''.
|
||||||
|
|
||||||
|
Used to derive the inbox scope key from a chain of fallbacks (the
|
||||||
|
confirmed reply email -> ``user.email`` -> ``user.login``).
|
||||||
|
``email_normalize`` lowercases the address and returns a falsy value for
|
||||||
|
anything that is not exactly one valid email — including a self-set
|
||||||
|
wildcard like ``%`` — so the value fed into ``build_scope_domain`` can
|
||||||
|
never widen the scope. Pairs with :func:`escape_like` as defense in depth
|
||||||
|
against the ``partner_email =ilike`` IDOR.
|
||||||
|
"""
|
||||||
|
for candidate in candidates:
|
||||||
|
normalized = email_normalize(candidate or '')
|
||||||
|
if normalized:
|
||||||
|
return normalized
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
def build_ticket_vals(kind, subject, body_html, team_id, client_label,
|
def build_ticket_vals(kind, subject, body_html, team_id, client_label,
|
||||||
reporter_name, reporter_email, company_name):
|
reporter_name, reporter_email, company_name):
|
||||||
"""Construct the `helpdesk.ticket` create vals for a forwarded report.
|
"""Construct the `helpdesk.ticket` create vals for a forwarded report.
|
||||||
|
|||||||
Reference in New Issue
Block a user