feat(fusion_login_audit): hook unknown-user failures via _login

Overrides res.users._login. When the login string does not resolve to
any user, super() raises AccessDenied; we record a row with user_id=NULL
and failure_reason="unknown_user", then re-raise. Closes the gap where
typo'd or scanned logins would otherwise vanish from the audit trail.

The existing _fc_record_login_event helper writes through an independent
registry.cursor(), so the audit row survives the rollback that follows
the re-raised AccessDenied.

Note: in Odoo 19 _login is a plain instance method (not the classmethod
it was in earlier versions) and takes (credential, user_agent_env). The
original plan was written for the classmethod signature; corrected here
and recorded in CLAUDE.md rule #10 so future-Claude does not waste time
re-discovering it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-26 21:11:57 -04:00
parent 0e6ebe7bc6
commit a7cf44249d
3 changed files with 59 additions and 0 deletions

View File

@@ -165,3 +165,31 @@ class ResUsers(models.Model):
_credential=credential,
)
raise
def _login(self, credential, user_agent_env):
"""Catch the unknown-user branch of upstream _login.
In Odoo 19 ``_login`` is an *instance* method (not a classmethod as in
earlier versions). Upstream raises ``AccessDenied`` when the login
string does not resolve to any user — at that point no user record
exists, so the ``bad_password`` path in ``_check_credentials`` never
fires. We catch the propagating exception here and write a row with
``user_id=NULL`` and ``failure_reason='unknown_user'``.
``_fc_record_login_event`` already writes through an INDEPENDENT
cursor (``self.env.registry.cursor()``), so the audit row survives
the outer transaction rollback that follows the re-raised
``AccessDenied``. We never block the re-raise: any audit-side
exception is caught + logged inside the helper.
"""
try:
return super()._login(credential, user_agent_env)
except AccessDenied:
self._fc_record_login_event(
result='failure',
failure_reason='unknown_user',
user_id=False,
attempted_login=(credential or {}).get('login') or 'unknown',
_credential=credential,
)
raise