fix(shopfloor-sec): narrow kiosk ir.config_parameter scope + doc accuracy

Code-review findings on Phase A (Tablet PIN Session Redesign):

I1: Security XML comment now honestly describes the kiosk as Internal
User + explicit reads, not 'near-zero ACL'. base.group_user is kept
(required for auth='user' HTTP routes to function) but the comment
no longer overstates how locked-down the kiosk is.

I2: New ir.rule scopes the kiosk's ir.config_parameter read to keys
matching 'fp.tablet.%' or 'fp.shopfloor.%'. Combined with the
existing model-level read ACL, kiosk can no longer enumerate
third-party secrets (e.g. fusion_tasks.vapid_private_key) or
arbitrary API keys stored in ICP.

I3: post-migrate docstring now advises sysadmins to unlink the
plaintext ICP password row after kiosk tablets are paired, to
minimise plaintext-in-backups risk. Rotation procedure documented.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-24 12:22:40 -04:00
parent a52ef29a84
commit 0b92294586
2 changed files with 44 additions and 8 deletions

View File

@@ -10,8 +10,21 @@ After this hook runs, retrieve the kiosk password via:
'fp.tablet.kiosk_password'))"
Then sysadmin enters that password ONCE in the tablet browser to log
the kiosk session in. Browser cookie persists per the configured
session_db.session_lifetime.
the kiosk session in. Browser cookie persists per Odoo's configured
session lifetime.
Security note: the generated password is stored in plaintext in
ir.config_parameter so a sysadmin can retrieve it. After the kiosk
tablets are paired (browser cookies established), DELETE the ICP key
to remove the plaintext from the DB + future backups:
env['ir.config_parameter'].search([
('key', '=', 'fp.tablet.kiosk_password')
]).unlink()
If you ever need to re-pair a tablet later, rotate by setting a new
password on the fp_tablet_kiosk user form, then re-authenticate the
tablet browser with that new value.
"""
import logging
import secrets

View File

@@ -1,14 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Tablet kiosk group: orthogonal to the Fusion Plating role hierarchy.
NO privilege_id (would clutter the role picker). This group holds
the bare minimum ACL for the lock screen to render and accept PIN:
- read res.users (tile grid)
- read ir.config_parameter (idle/ceiling settings)
Nothing else. The dedicated fp_tablet_kiosk user is the only
expected member. -->
NO privilege_id (would clutter the role picker).
The dedicated fp_tablet_kiosk user inherits the standard Internal
User reads via base.group_user (required for any auth='user' HTTP
route to function). On top of that, this group grants explicit
read on res.users (tile grid) and a NARROWED read on
ir.config_parameter (whitelisted keys only — see ir.rule below).
No write access to anything; no read on business records
(fp.job, sale.order, fp.certificate, fp.part.catalog, etc.).
Threat model: a compromised kiosk session can enumerate users
and read whitelisted tablet/shopfloor config keys, nothing more.
-->
<record id="group_fp_tablet_kiosk" model="res.groups">
<field name="name">Tablet Kiosk Session</field>
<field name="sequence">100</field>
</record>
<!-- I2 fix: Narrow the kiosk's ir.config_parameter read to keys that
begin with fp.tablet. or fp.shopfloor. — prevents reading
third-party secrets like fusion_tasks.vapid_private_key or
arbitrary API keys stored in ICP. The CSV row that grants
model-level read still needs this rule to scope the matches. -->
<record id="rule_kiosk_ir_config_parameter_scoped" model="ir.rule">
<field name="name">Kiosk: read only fp.tablet/fp.shopfloor config keys</field>
<field name="model_id" ref="base.model_ir_config_parameter"/>
<field name="groups" eval="[(4, ref('fusion_plating_shopfloor.group_fp_tablet_kiosk'))]"/>
<field name="domain_force">['|', ('key', '=like', 'fp.tablet.%'), ('key', '=like', 'fp.shopfloor.%')]</field>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
</odoo>