feat(fusion_login_audit): settings model + page section
Four x_fc_* fields on res.config.settings backed by ir.config_parameter: retention_days (default 365, 0 = forever), alert_threshold (5), alert_window_min (15), alert_enabled (True). New "Login Audit" block on the General Settings page (gated by base.group_system on the block, NOT on the inherited view record per CLAUDE.md rule #11). CLAUDE.md gotchas added during this task: #5 Boolean config_parameter fields don't round-trip "False" as a string — IrConfigParameter.set_param deletes the row on falsy. Test with assertFalse, never assertEqual(..., "False"). #6 ir.ui.view uses group_ids (Odoo 19 rename mirrored from res.users). Setting groups_id on an ir.ui.view record raises ValueError at install. (The XML attribute groups="..." on inner nodes is unrelated and still works.) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +19,7 @@ bursts. Daily retention cron honours a configurable horizon.
|
||||
'author': 'Nexa Systems Inc.',
|
||||
'website': 'https://nexasystems.ca',
|
||||
'license': 'OPL-1',
|
||||
'depends': ['base', 'mail'],
|
||||
'depends': ['base', 'mail', 'base_setup'],
|
||||
'external_dependencies': {
|
||||
'python': ['user_agents'],
|
||||
},
|
||||
@@ -28,6 +28,7 @@ bursts. Daily retention cron honours a configurable horizon.
|
||||
'security/security.xml',
|
||||
'views/fusion_login_audit_views.xml',
|
||||
'views/res_users_views.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/menus.xml',
|
||||
],
|
||||
'installable': True,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import fusion_login_audit
|
||||
from . import res_users
|
||||
from . import res_config_settings
|
||||
|
||||
31
fusion_login_audit/models/res_config_settings.py
Normal file
31
fusion_login_audit/models/res_config_settings.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
x_fc_login_audit_retention_days = fields.Integer(
|
||||
string='Login Audit Retention (days)',
|
||||
default=365,
|
||||
config_parameter='fusion_login_audit.retention_days',
|
||||
help='Login audit rows older than this are deleted by the nightly '
|
||||
'cron. Set to 0 to keep forever.',
|
||||
)
|
||||
x_fc_login_audit_alert_threshold = fields.Integer(
|
||||
string='Alert After N Consecutive Failures',
|
||||
default=5,
|
||||
config_parameter='fusion_login_audit.alert_threshold',
|
||||
help='When this many failures for the same attempted login occur '
|
||||
'within the alert window, Settings admins receive one email.',
|
||||
)
|
||||
x_fc_login_audit_alert_window_min = fields.Integer(
|
||||
string='Alert Window (minutes)',
|
||||
default=15,
|
||||
config_parameter='fusion_login_audit.alert_window_min',
|
||||
)
|
||||
x_fc_login_audit_alert_enabled = fields.Boolean(
|
||||
string='Send Failed-Login Alerts',
|
||||
default=True,
|
||||
config_parameter='fusion_login_audit.alert_enabled',
|
||||
)
|
||||
@@ -266,3 +266,21 @@ class TestFusionLoginAuditModel(TransactionCase):
|
||||
self.assertEqual(action['type'], 'ir.actions.act_window')
|
||||
# Domain must filter to this user
|
||||
self.assertIn(('user_id', '=', user.id), action['domain'])
|
||||
|
||||
def test_settings_round_trip(self):
|
||||
"""Writing settings persists them via ir.config_parameter."""
|
||||
Settings = self.env['res.config.settings'].sudo()
|
||||
Settings.create({
|
||||
'x_fc_login_audit_retention_days': 90,
|
||||
'x_fc_login_audit_alert_threshold': 3,
|
||||
'x_fc_login_audit_alert_window_min': 5,
|
||||
'x_fc_login_audit_alert_enabled': False,
|
||||
}).execute()
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
self.assertEqual(ICP.get_param('fusion_login_audit.retention_days'), '90')
|
||||
self.assertEqual(ICP.get_param('fusion_login_audit.alert_threshold'), '3')
|
||||
self.assertEqual(ICP.get_param('fusion_login_audit.alert_window_min'), '5')
|
||||
# Odoo's set_param deletes the row when the value is falsy, so a
|
||||
# Boolean field set to False yields get_param() == False (Python
|
||||
# bool, the default), not the string 'False'.
|
||||
self.assertFalse(ICP.get_param('fusion_login_audit.alert_enabled'))
|
||||
|
||||
36
fusion_login_audit/views/res_config_settings_views.xml
Normal file
36
fusion_login_audit/views/res_config_settings_views.xml
Normal file
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_res_config_settings_form_login_audit" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.form.login.audit</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="base_setup.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//block[@id='user_default_rights']" position="after">
|
||||
<block title="Login Audit"
|
||||
name="login_audit_block"
|
||||
groups="base.group_system">
|
||||
<setting id="login_audit_retention"
|
||||
string="Retention (days)"
|
||||
help="0 = keep forever">
|
||||
<field name="x_fc_login_audit_retention_days"/>
|
||||
</setting>
|
||||
<setting id="login_audit_alert_enabled"
|
||||
string="Send failed-login alerts"
|
||||
help="Email Settings admins when consecutive failures cross the threshold">
|
||||
<field name="x_fc_login_audit_alert_enabled"/>
|
||||
</setting>
|
||||
<setting id="login_audit_alert_threshold"
|
||||
string="Alert threshold (failures)">
|
||||
<field name="x_fc_login_audit_alert_threshold"/>
|
||||
</setting>
|
||||
<setting id="login_audit_alert_window"
|
||||
string="Alert window (minutes)">
|
||||
<field name="x_fc_login_audit_alert_window_min"/>
|
||||
</setting>
|
||||
</block>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user