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:
@@ -197,3 +197,33 @@ class TestFusionLoginAuditModel(TransactionCase):
|
||||
'database'):
|
||||
self.assertNotIn(secret, (row[fname] or ''),
|
||||
f"Password leaked into field {fname}")
|
||||
|
||||
def test_unknown_user_writes_failure_row(self):
|
||||
"""A login attempt for a username that does not exist gets logged
|
||||
with user_id=NULL and failure_reason='unknown_user'."""
|
||||
from odoo.exceptions import AccessDenied
|
||||
bogus = 'this-user-does-not-exist@example.com'
|
||||
Audit = self.env['fusion.login.audit'].sudo()
|
||||
before = Audit.search_count([('attempted_login', '=', bogus)])
|
||||
# NB: manual try/except instead of assertRaises — see comment in
|
||||
# test_bad_password_writes_failure_row. _login is an instance method
|
||||
# in Odoo 19 (not a classmethod as in earlier versions); we call it
|
||||
# on the empty recordset of res.users, which matches what
|
||||
# authenticate() does internally.
|
||||
raised = False
|
||||
try:
|
||||
self.env['res.users']._login(
|
||||
{'login': bogus, 'password': 'whatever',
|
||||
'type': 'password'},
|
||||
{'interactive': False},
|
||||
)
|
||||
except AccessDenied:
|
||||
raised = True
|
||||
self.assertTrue(raised, "AccessDenied must propagate after the audit write")
|
||||
after = Audit.search_count([('attempted_login', '=', bogus)])
|
||||
self.assertEqual(after, before + 1)
|
||||
row = Audit.search([('attempted_login', '=', bogus)],
|
||||
order='event_time desc', limit=1)
|
||||
self.assertFalse(row.user_id)
|
||||
self.assertEqual(row.failure_reason, 'unknown_user')
|
||||
self.assertEqual(row.result, 'failure')
|
||||
|
||||
Reference in New Issue
Block a user