# -*- coding: utf-8 -*- from odoo.exceptions import AccessError from odoo.tests.common import TransactionCase, tagged @tagged('post_install', '-at_install') class TestFusionLoginAuditSecurity(TransactionCase): """Tests for the layered protection on `fusion.login.audit`: Layer 1 — ACL (security/ir.model.access.csv): grants read-only access to `base.group_system` and nothing to any other group. Blocks write/create/ unlink for everyone via the ORM regardless of `sudo()`. Layer 2 — Record rule (security/security.xml): group-specific rule that grants admins an unrestricted domain (`[(1,'=',1)]`). The rule does NOT actively restrict non-admins — Odoo's semantics for a group-scoped rule is "the rule only applies to users in that group". Non-admins are gated purely by the ACL, which denies them everything. The rule's value is documentation + future-proofing (it keeps admin access explicit if the ACL is ever loosened with a per-group read row; the admin path remains explicit and self-documenting). It is NOT a security gate the ACL relies on. Test naming reflects which layer actually does the work: - test_acl_blocks_* — exercises Layer 1 (ACL alone is sufficient). - test_admin_can_read_through_acl_and_rule — exercises both layers in the positive path (admin must satisfy ACL grant AND the admin-scoped rule's domain). """ def setUp(self): super().setUp() self.audit_row = self.env['fusion.login.audit'].sudo().create({ 'attempted_login': 'sec-test@example.com', 'result': 'success', 'database': self.env.cr.dbname, }) # Internal non-admin user (active employee, not a Settings admin). self.regular_user = self.env['res.users'].sudo().create({ 'name': 'Regular Tester', 'login': 'regular-tester@example.com', 'password': 'regular-tester-pw-1', 'group_ids': [(6, 0, [self.env.ref('base.group_user').id])], }) # Portal user (share=True) — must not see audit data either. self.portal_user = self.env['res.users'].sudo().create({ 'name': 'Portal Tester', 'login': 'portal-tester@example.com', 'password': 'portal-tester-pw-1', 'group_ids': [(6, 0, [self.env.ref('base.group_portal').id])], }) def test_admin_can_read_through_acl_and_rule(self): """A Settings admin satisfies both the ACL (grants read) and the record rule (admin-only domain), so the read succeeds.""" admin = self.env.ref('base.user_admin') rec = self.audit_row.with_user(admin).read(['attempted_login']) self.assertEqual(rec[0]['attempted_login'], 'sec-test@example.com') def test_acl_blocks_read_for_regular_user(self): """A `base.group_user` member has no ACL grant on the model. The ACL alone denies the read; the record rule never gets consulted.""" with self.assertRaises(AccessError): self.audit_row.with_user(self.regular_user).read(['attempted_login']) def test_acl_blocks_read_for_portal_user(self): """A `base.group_portal` (share=True) user has no ACL grant either. Audit data must never leak to a portal user — IP and attempted_login are sensitive.""" with self.assertRaises(AccessError): self.audit_row.with_user(self.portal_user).read(['attempted_login']) def test_acl_blocks_write_for_admin(self): """Even Settings admins cannot write — the ACL grants no group any write permission on this model (audit log is append-only). The rule's `perm_write=False` means 'rule does not constrain this op', so this denial is the ACL's work alone.""" admin = self.env.ref('base.user_admin') with self.assertRaises(AccessError): self.audit_row.with_user(admin).write({'attempted_login': 'tampered'}) def test_acl_blocks_unlink_for_admin(self): """Append-only also at the unlink boundary. ACL grants no group delete permission; the record rule's `perm_unlink=False` exempts it from gating this op.""" admin = self.env.ref('base.user_admin') with self.assertRaises(AccessError): self.audit_row.with_user(admin).unlink() # Note: a "rule actively blocks non-admins" test was attempted but # removed once the actual Odoo semantics were verified. A group-scoped # rule (groups=[base.group_system]) only applies to users in that group. # Granting a base.group_user member an ACL read row would let them read # rows — the rule does not filter them. To make the rule truly restrictive # we would need a global rule (groups=[]) with domain [(0,'=',1)] paired # with the admin grant. That is a security-model redesign and out of # scope for T3. The ACL already provides the actual gate.