fix(shopfloor): Phase B review findings — C1/I1/I2/I3/M1
C1: Add placeholder fp_tablet_cron.xml + fp_tablet_session_event_views.xml
so the module is installable now (real content lands in Phase C task C4
and Phase E task E1 respectively).
I1: test_tablet_pin_auth_manager now passes {} (not self.env) as the
env arg to _check_credentials — matches what request.session.authenticate
provides and what the base implementation expects.
I2: Auth manager role check now uses user_sudo.all_group_ids (transitive)
instead of group_ids (direct) per CLAUDE.md rules 13l + 23. Owner users
who hold Owner directly still match all 5 shop-branch xmlids via the
implication chain.
I3: fp.tablet.session.event gains Python-layer write() + unlink()
overrides that always raise AccessError unless the explicit
fp_tablet_audit_admin_write / fp_tablet_audit_admin_purge context flag
is set. Closes the gap between the model's append-only docstring and
its actual enforcement (ACL-only previously).
M1: Hoisted 'from odoo.exceptions import AccessDenied' to top-of-file
imports next to existing UserError import.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Placeholder for cron records added in Phase C task C4
|
||||
(force-lock stale tablet sessions). File registered in manifest
|
||||
to keep the data list stable across phase boundaries. -->
|
||||
</odoo>
|
||||
@@ -7,7 +7,8 @@ write/unlink/edit are forbidden to anyone except root via direct SQL.
|
||||
|
||||
Spec section 4: docs/superpowers/specs/2026-05-24-tablet-pin-session-redesign-design.md
|
||||
"""
|
||||
from odoo import api, fields, models
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import AccessError
|
||||
|
||||
|
||||
class FpTabletSessionEvent(models.Model):
|
||||
@@ -88,3 +89,27 @@ class FpTabletSessionEvent(models.Model):
|
||||
)
|
||||
|
||||
notes = fields.Text(readonly=True)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Append-only enforcement at the Python layer
|
||||
# ACL grants ONLY read on the model (Phase A CSV); these overrides
|
||||
# block write/unlink even for sudo() callers that would bypass ACL.
|
||||
# The only legitimate path for purging the audit log is a
|
||||
# context-flagged admin sweep (e.g. a future retention cron).
|
||||
# ------------------------------------------------------------------
|
||||
def write(self, vals):
|
||||
if not self.env.context.get('fp_tablet_audit_admin_write'):
|
||||
raise AccessError(_(
|
||||
"fp.tablet.session.event rows are append-only. "
|
||||
"Use the audit log; do not modify historical entries."
|
||||
))
|
||||
return super().write(vals)
|
||||
|
||||
def unlink(self):
|
||||
if not self.env.context.get('fp_tablet_audit_admin_purge'):
|
||||
raise AccessError(_(
|
||||
"fp.tablet.session.event rows are append-only. "
|
||||
"Delete only via the retention cron with the explicit "
|
||||
"admin-purge context flag."
|
||||
))
|
||||
return super().unlink()
|
||||
|
||||
@@ -12,7 +12,7 @@ import hashlib
|
||||
import secrets
|
||||
|
||||
from odoo import _, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.exceptions import AccessDenied, UserError
|
||||
|
||||
# PBKDF2 iteration count. ~50ms verify on entech-class hardware. Safe
|
||||
# against brute-force even if the DB leaks.
|
||||
@@ -151,7 +151,6 @@ class ResUsers(models.Model):
|
||||
See docs/superpowers/specs/2026-05-24-tablet-pin-session-redesign-design.md
|
||||
Section 2 — Auth path.
|
||||
"""
|
||||
from odoo.exceptions import AccessDenied
|
||||
if isinstance(credential, dict) and credential.get('type') == 'fp_tablet_pin':
|
||||
login = credential.get('login')
|
||||
pin = credential.get('pin')
|
||||
@@ -160,7 +159,10 @@ class ResUsers(models.Model):
|
||||
user_sudo = self.sudo().search([('login', '=', login)], limit=1)
|
||||
if not user_sudo or not user_sudo.active:
|
||||
raise AccessDenied()
|
||||
# Must hold a shop-branch role (otherwise they can't operate the tablet)
|
||||
# Must hold a shop-branch role (transitively — all_group_ids follows
|
||||
# the implication chain so users who hold Owner directly still match
|
||||
# the Technician/Manager checks below). Matches has_group() semantics
|
||||
# and is futureproof against role-graph edits (CLAUDE.md rules 13l + 23).
|
||||
shop_branch_xmlids = (
|
||||
'fusion_plating.group_fp_technician',
|
||||
'fusion_plating.group_fp_shop_manager_v2',
|
||||
@@ -174,7 +176,7 @@ class ResUsers(models.Model):
|
||||
for x in shop_branch_xmlids
|
||||
) if g
|
||||
}
|
||||
user_group_ids = set(user_sudo.group_ids.ids)
|
||||
user_group_ids = set(user_sudo.all_group_ids.ids)
|
||||
if not (shop_branch_ids & user_group_ids):
|
||||
raise AccessDenied()
|
||||
# Verify the PIN hash. verify_tablet_pin already exists.
|
||||
|
||||
@@ -28,7 +28,7 @@ class TestTabletPinAuthManager(TransactionCase):
|
||||
def _check(self, login, pin):
|
||||
return self.env['res.users'].sudo()._check_credentials(
|
||||
{'type': 'fp_tablet_pin', 'login': login, 'pin': pin},
|
||||
self.env,
|
||||
{},
|
||||
)
|
||||
|
||||
def test_correct_pin_succeeds(self):
|
||||
@@ -72,7 +72,7 @@ class TestTabletPinAuthManager(TransactionCase):
|
||||
self.env['res.users'].sudo()._check_credentials(
|
||||
{'type': 'password', 'login': 'authmgr_tech@example.com',
|
||||
'password': 'wrong'},
|
||||
self.env,
|
||||
{},
|
||||
)
|
||||
except AccessDenied:
|
||||
pass # expected — wrong password
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Placeholder for fp.tablet.session.event list + form views,
|
||||
action, and Owner-only menu added in Phase E task E1.
|
||||
File registered in manifest to keep the data list stable
|
||||
across phase boundaries. -->
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user