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'))" 'fp.tablet.kiosk_password'))"
Then sysadmin enters that password ONCE in the tablet browser to log Then sysadmin enters that password ONCE in the tablet browser to log
the kiosk session in. Browser cookie persists per the configured the kiosk session in. Browser cookie persists per Odoo's configured
session_db.session_lifetime. 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 logging
import secrets import secrets

View File

@@ -1,14 +1,37 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<odoo> <odoo>
<!-- Tablet kiosk group: orthogonal to the Fusion Plating role hierarchy. <!-- Tablet kiosk group: orthogonal to the Fusion Plating role hierarchy.
NO privilege_id (would clutter the role picker). This group holds NO privilege_id (would clutter the role picker).
the bare minimum ACL for the lock screen to render and accept PIN:
- read res.users (tile grid) The dedicated fp_tablet_kiosk user inherits the standard Internal
- read ir.config_parameter (idle/ceiling settings) User reads via base.group_user (required for any auth='user' HTTP
Nothing else. The dedicated fp_tablet_kiosk user is the only route to function). On top of that, this group grants explicit
expected member. --> 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"> <record id="group_fp_tablet_kiosk" model="res.groups">
<field name="name">Tablet Kiosk Session</field> <field name="name">Tablet Kiosk Session</field>
<field name="sequence">100</field> <field name="sequence">100</field>
</record> </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> </odoo>