This commit is contained in:
gsinghpal
2026-05-30 20:59:59 -04:00
parent 55da42e91f
commit 5c1f60b3b8
17 changed files with 147 additions and 56 deletions

View File

@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0)
{
'name': 'Fusion Login Audit',
'version': '19.0.1.0.0',
'version': '19.0.1.1.0',
'category': 'Tools',
'summary': 'Durable login audit log with geo-enrichment, retention, and failure alerts.',
'description': """

View File

@@ -72,6 +72,17 @@ class FusionLoginAudit(models.Model):
string='Device Type', default='unknown',
)
database = fields.Char(string='Database', size=64)
login_kind = fields.Selection(
[
('interactive', 'Interactive'),
('service', 'Service / Automation'),
],
string='Login Kind', default='interactive', index=True,
help="Interactive = a real browser/HTTP login. Service = server-to-server "
"auth (XML-RPC/JSON-RPC, cron, inter-instance sync) with no HTTP "
"request. Service logins are hidden from the default Login Events "
"view to keep it focused on real user activity.",
)
# Odoo 19 replaces the legacy `_sql_constraints = [...]` list with
# declarative `models.Constraint` attributes. The plan template used the

View File

@@ -86,11 +86,16 @@ class ResUsers(models.Model):
else:
vals['device_type'] = 'unknown'
vals['geo_lookup_state'] = 'pending'
# A live HTTP request means a real browser/interactive login.
vals['login_kind'] = 'interactive'
else:
vals['ip_address'] = 'internal'
vals['user_agent_raw'] = '<no-request>'
vals['device_type'] = 'unknown'
vals['geo_lookup_state'] = 'internal'
# No HTTP request = server-to-server auth (XML-RPC/JSON-RPC, cron,
# inter-instance sync). Tagged so the default view can hide it.
vals['login_kind'] = 'service'
# _credential is accepted in the signature so callers (T6 _check_credentials,
# T7 _login) can hand the dict in without filtering. The helper deliberately
@@ -388,5 +393,6 @@ class ResUsers(models.Model):
'view_mode': 'list,form',
'domain': [('user_id', '=', self.id)],
'context': {'create': False, 'edit': False, 'delete': False,
'default_user_id': self.id},
'default_user_id': self.id,
'search_default_filter_interactive': 1},
}

View File

@@ -74,6 +74,7 @@ class TestFusionLoginAuditModel(TransactionCase):
self.assertEqual(vals['ip_address'], 'internal')
self.assertEqual(vals['user_agent_raw'], '<no-request>')
self.assertEqual(vals['geo_lookup_state'], 'internal')
self.assertEqual(vals['login_kind'], 'service')
self.assertEqual(vals['database'], self.env.cr.dbname)
def test_build_event_vals_parses_user_agent(self):
@@ -91,6 +92,7 @@ class TestFusionLoginAuditModel(TransactionCase):
self.assertIn('Windows', vals['os'])
self.assertEqual(vals['device_type'], 'desktop')
self.assertEqual(vals['geo_lookup_state'], 'pending')
self.assertEqual(vals['login_kind'], 'interactive')
def test_build_event_vals_strips_password(self):
"""If a credential dict sneaks in, no password leaks into vals."""

View File

@@ -14,6 +14,7 @@
<field name="user_id"/>
<field name="attempted_login"/>
<field name="result" widget="badge"/>
<field name="login_kind" optional="show"/>
<field name="failure_reason"/>
<field name="ip_address"/>
<field name="country_code"/>
@@ -36,6 +37,7 @@
<group string="Event">
<field name="event_time" readonly="1"/>
<field name="result" readonly="1" widget="badge"/>
<field name="login_kind" readonly="1"/>
<field name="failure_reason" readonly="1"/>
<field name="user_id" readonly="1"/>
<field name="attempted_login" readonly="1"/>
@@ -77,6 +79,11 @@
<filter name="filter_failure" string="Failures"
domain="[('result','=','failure')]"/>
<separator/>
<filter name="filter_interactive" string="Interactive"
domain="[('login_kind','=','interactive')]"/>
<filter name="filter_service" string="Service / Automation"
domain="[('login_kind','=','service')]"/>
<separator/>
<filter name="filter_24h" string="Last 24 hours"
domain="[('event_time','&gt;=', (context_today() - relativedelta(days=1)).strftime('%Y-%m-%d 00:00:00'))]"/>
<filter name="filter_7d" string="Last 7 days"
@@ -93,6 +100,8 @@
context="{'group_by': 'country_code'}"/>
<filter name="group_ip" string="IP"
context="{'group_by': 'ip_address'}"/>
<filter name="group_kind" string="Login Kind"
context="{'group_by': 'login_kind'}"/>
</group>
</search>
</field>
@@ -104,7 +113,10 @@
<field name="res_model">fusion.login.audit</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_fusion_login_audit_search"/>
<field name="context">{}</field>
<!-- Default to interactive logins only; service/automation auth
(inter-instance sync, cron, XML-RPC) is hidden but reachable via
the "Service / Automation" filter. -->
<field name="context">{'search_default_filter_interactive': 1}</field>
</record>
<record id="action_fusion_login_audit_failures_24h" model="ir.actions.act_window">