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
|
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):
|
class FpTabletSessionEvent(models.Model):
|
||||||
@@ -88,3 +89,27 @@ class FpTabletSessionEvent(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
notes = fields.Text(readonly=True)
|
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
|
import secrets
|
||||||
|
|
||||||
from odoo import _, fields, models
|
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
|
# PBKDF2 iteration count. ~50ms verify on entech-class hardware. Safe
|
||||||
# against brute-force even if the DB leaks.
|
# 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
|
See docs/superpowers/specs/2026-05-24-tablet-pin-session-redesign-design.md
|
||||||
Section 2 — Auth path.
|
Section 2 — Auth path.
|
||||||
"""
|
"""
|
||||||
from odoo.exceptions import AccessDenied
|
|
||||||
if isinstance(credential, dict) and credential.get('type') == 'fp_tablet_pin':
|
if isinstance(credential, dict) and credential.get('type') == 'fp_tablet_pin':
|
||||||
login = credential.get('login')
|
login = credential.get('login')
|
||||||
pin = credential.get('pin')
|
pin = credential.get('pin')
|
||||||
@@ -160,7 +159,10 @@ class ResUsers(models.Model):
|
|||||||
user_sudo = self.sudo().search([('login', '=', login)], limit=1)
|
user_sudo = self.sudo().search([('login', '=', login)], limit=1)
|
||||||
if not user_sudo or not user_sudo.active:
|
if not user_sudo or not user_sudo.active:
|
||||||
raise AccessDenied()
|
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 = (
|
shop_branch_xmlids = (
|
||||||
'fusion_plating.group_fp_technician',
|
'fusion_plating.group_fp_technician',
|
||||||
'fusion_plating.group_fp_shop_manager_v2',
|
'fusion_plating.group_fp_shop_manager_v2',
|
||||||
@@ -174,7 +176,7 @@ class ResUsers(models.Model):
|
|||||||
for x in shop_branch_xmlids
|
for x in shop_branch_xmlids
|
||||||
) if g
|
) 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):
|
if not (shop_branch_ids & user_group_ids):
|
||||||
raise AccessDenied()
|
raise AccessDenied()
|
||||||
# Verify the PIN hash. verify_tablet_pin already exists.
|
# Verify the PIN hash. verify_tablet_pin already exists.
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class TestTabletPinAuthManager(TransactionCase):
|
|||||||
def _check(self, login, pin):
|
def _check(self, login, pin):
|
||||||
return self.env['res.users'].sudo()._check_credentials(
|
return self.env['res.users'].sudo()._check_credentials(
|
||||||
{'type': 'fp_tablet_pin', 'login': login, 'pin': pin},
|
{'type': 'fp_tablet_pin', 'login': login, 'pin': pin},
|
||||||
self.env,
|
{},
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_correct_pin_succeeds(self):
|
def test_correct_pin_succeeds(self):
|
||||||
@@ -72,7 +72,7 @@ class TestTabletPinAuthManager(TransactionCase):
|
|||||||
self.env['res.users'].sudo()._check_credentials(
|
self.env['res.users'].sudo()._check_credentials(
|
||||||
{'type': 'password', 'login': 'authmgr_tech@example.com',
|
{'type': 'password', 'login': 'authmgr_tech@example.com',
|
||||||
'password': 'wrong'},
|
'password': 'wrong'},
|
||||||
self.env,
|
{},
|
||||||
)
|
)
|
||||||
except AccessDenied:
|
except AccessDenied:
|
||||||
pass # expected — wrong password
|
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