fix(fusion_login_audit): avoid duplicate row on bad-password

When the login string resolves to an existing user and the password is
wrong, BOTH overrides used to write a failure row:
  - _check_credentials wrapper: result=failure, reason=bad_password
  - _login wrapper (catching the propagating AccessDenied): result=
    failure, reason=unknown_user

Discovered in production smoke on westin-v19 after the deploy: a
single failed login for info@gsafinancialconsulting.com produced two
audit rows (one bad_password, one unknown_user). The unknown_user
label was wrong — the user IS in the system.

Fix: _login now checks whether the login string resolves to any user
BEFORE writing the unknown_user row. If yes, _check_credentials
already logged the attempt and _login skips. If no, the user lookup
in super() failed and _login is the only chance to log.

Regression test test_login_known_user_bad_password_single_row asserts
exactly one row per attempt and that the row carries bad_password
(not unknown_user) when the user exists.

30 tests green locally; production smoke on westin-v19 confirms:
one row per failed login, bad_password, IP 172.18.0.1 captured.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-26 23:01:09 -04:00
parent 5d9609b5ee
commit 9df3262d30
2 changed files with 71 additions and 17 deletions

View File

@@ -237,6 +237,44 @@ class TestFusionLoginAuditModel(TransactionCase):
self.assertEqual(row.failure_reason, 'unknown_user')
self.assertEqual(row.result, 'failure')
def test_login_known_user_bad_password_single_row(self):
"""When _login is the entry point for an existing user with the
wrong password, only ONE failure row is written (bad_password from
_check_credentials) — NOT two (bad_password + unknown_user). The
unknown_user branch must only fire when the login string does not
resolve to any user.
Regression test for the duplicate-row bug discovered during the
production deploy smoke on westin-v19: a single failed login for
an existing user was creating two audit rows.
"""
from odoo.exceptions import AccessDenied
user = self.env['res.users'].sudo().create({
'name': 'NoDupTester',
'login': 'nodup-tester@example.com',
'password': 'nodup-tester-pw-1',
})
Audit = self.env['fusion.login.audit'].sudo()
before = Audit.search_count([('attempted_login', '=', user.login)])
raised = False
try:
self.env['res.users']._login(
{'login': user.login, 'password': 'wrong-not-the-real-one',
'type': 'password'},
{'interactive': False},
)
except AccessDenied:
raised = True
self.assertTrue(raised)
after = Audit.search_count([('attempted_login', '=', user.login)])
self.assertEqual(after - before, 1,
"Exactly one row per failed login attempt — not two")
row = Audit.search([('attempted_login', '=', user.login)],
order='event_time desc', limit=1)
self.assertEqual(row.failure_reason, 'bad_password',
"Existing-user failure must record bad_password, "
"not unknown_user (the user IS in the system)")
def test_computed_last_successful_login(self):
"""x_fc_last_successful_login reflects the latest success row."""
user = self.env['res.users'].sudo().create({