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:
gsinghpal
2026-05-24 12:47:26 -04:00
parent 358b90516b
commit 765b095035
5 changed files with 47 additions and 7 deletions

View File

@@ -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>

View File

@@ -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()

View File

@@ -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.

View File

@@ -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

View File

@@ -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>