From 5e2e7ec29c84c6cecff7763333c05131fdb16046 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Wed, 27 May 2026 09:13:13 -0400 Subject: [PATCH] fix(fusion_helpdesk): define missing _norm_email helper (NameError on every inbox endpoint) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- fusion_helpdesk/__manifest__.py | 2 +- fusion_helpdesk/controllers/main.py | 2 +- fusion_helpdesk/tests/test_utils.py | 26 ++++++++++++++++++++++++++ fusion_helpdesk/utils.py | 19 +++++++++++++++++++ 4 files changed, 47 insertions(+), 2 deletions(-) diff --git a/fusion_helpdesk/__manifest__.py b/fusion_helpdesk/__manifest__.py index 9f24d860..59190928 100644 --- a/fusion_helpdesk/__manifest__.py +++ b/fusion_helpdesk/__manifest__.py @@ -3,7 +3,7 @@ # License OPL-1 (Odoo Proprietary License v1.0) { 'name': 'Fusion Helpdesk Reporter', - 'version': '19.0.1.4.0', + 'version': '19.0.1.4.1', 'category': 'Productivity', 'summary': 'One-click in-app bug reporting & feature requesting — ' 'auto-creates a helpdesk.ticket on a central Odoo Helpdesk.', diff --git a/fusion_helpdesk/controllers/main.py b/fusion_helpdesk/controllers/main.py index 58612b59..4e92c90a 100644 --- a/fusion_helpdesk/controllers/main.py +++ b/fusion_helpdesk/controllers/main.py @@ -22,7 +22,6 @@ from urllib.parse import urljoin, urlparse from odoo import _, http from odoo.exceptions import UserError from odoo.http import request -from odoo.tools import email_normalize from odoo.addons.fusion_helpdesk.utils import ( build_ticket_vals, @@ -30,6 +29,7 @@ from odoo.addons.fusion_helpdesk.utils import ( is_public_message, compute_unread_count, escape_like, + _norm_email, ) _logger = logging.getLogger(__name__) diff --git a/fusion_helpdesk/tests/test_utils.py b/fusion_helpdesk/tests/test_utils.py index 30fa6d3b..3943b68e 100644 --- a/fusion_helpdesk/tests/test_utils.py +++ b/fusion_helpdesk/tests/test_utils.py @@ -14,6 +14,7 @@ from odoo.addons.fusion_helpdesk.utils import ( build_scope_domain, is_public_message, compute_unread_count, + _norm_email, ) @@ -103,3 +104,28 @@ class TestMessageFilterAndUnread(TransactionCase): def test_unread_count_unseen_ticket_counts(self): tickets = [{'id': 9, 'last_support_msg_id': 4}] 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')) diff --git a/fusion_helpdesk/utils.py b/fusion_helpdesk/utils.py index 28b81310..1c0bcc02 100644 --- a/fusion_helpdesk/utils.py +++ b/fusion_helpdesk/utils.py @@ -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 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 # "match everything". An empty string in `=`/`=ilike` would match rows @@ -29,6 +30,24 @@ def escape_like(value): 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, reporter_name, reporter_email, company_name): """Construct the `helpdesk.ticket` create vals for a forwarded report.