changes
This commit is contained in:
@@ -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': """
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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},
|
||||
}
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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','>=', (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">
|
||||
|
||||
Reference in New Issue
Block a user