Skip to content

feat(spp_pii_encryption): re-add migration wizard; mask registry ID numbers - #452

Merged
gonzalesedwin1123 merged 7 commits into
19.0from
migrate-spp-pii-encryption-pr3
Aug 25, 2026
Merged

feat(spp_pii_encryption): re-add migration wizard; mask registry ID numbers#452
gonzalesedwin1123 merged 7 commits into
19.0from
migrate-spp-pii-encryption-pr3

Conversation

@gonzalesedwin1123

@gonzalesedwin1123 gonzalesedwin1123 commented Aug 25, 2026

Copy link
Copy Markdown
Member

What

PR3 of the PII sequence — re-adds the encryption migration wizard to spp_pii_encryption and wires the masked_char widget into a real view via a new applier module. Completes the ADR-011/ADR-012 foundation chain:

Stacked on #233: base is migrate-spp-data-classification until #233 merges (the wizard scans [("is_pii", "=", True)], which #233 provides). After #233 lands this branch will be rebased --onto 19.0 and retargeted; the diff here shows only PR3's changes.

1. spp_pii_encryption → 19.0.2.0.0: migration wizard (scan / dry-run / migrate)

Ported from openspp-modules with deliberate changes:

  • No in-app rollback, no plaintext backup table. The source rollback never worked — it relied on a skip_encryption context no mixin implements (writing plaintext back just re-encrypts it), and the backup model persisted the exact plaintext values being encrypted, unencrypted, indefinitely — directly against ADR-012's own threat model ("Backup exposure → Encrypted backups"). The Migrate button now demands a database snapshot instead.
  • Honest batching: the source processed only the first batch_size records while reporting "Migration complete"; the wizard now loops until each field is exhausted, excluding failed records from the domain so one broken record can't loop forever, flushing/invalidating per batch.
  • Operator-safe scan: ir.model records are readable only by the Access Rights group, so the scan reads the classification's stored model_name and logs-and-skips models the operator can't search_count (the encryption admin is deliberately not a system admin — see feat(spp_pii_encryption): port PII encryption core from openspp-modules #232).
  • Also: batch_size validated positive, user-facing strings translated, and the model_name/model_id "Model" label clash on spp.field.encryption.config fixed (same fix as feat(spp_data_classification): port classification registry from openspp-modules #233's registry models).

Version goes to 19.0.2.0.0 to clear the openspp-modules baseline (19.0.1.0.0, which shipped the wizard); HISTORY fragment included.

2. New module: spp_registry_encryption (19.0.1.0.0)

First consumer of the PR2 groups and the PR1 widget: registrant ID numbers (spp.registry.id.value) render masked (••••-••••-1234) in both Identity tabs of spp_registry.view_individuals_form, with a reveal control gated by spp_data_classification.group_pii_full_access_admin and each reveal audited in spp.pii.audit.log.

Framing (per the widget-honesty findings, #451 items 6–7): this is display de-emphasis, not an access control. The widget masks readonly display; entering the editable cell shows the value, and the plaintext reaches the browser via the normal record read either way — the access boundary remains the record's ACLs. What it buys is shoulder-surfing/casual-exposure protection plus an audit trail for deliberate reveals. The module description states this explicitly. The mask keeps the last-4 pattern the classification registry itself seeds for national IDs; changing the platform default mask is #451 item 7. Server-side field-level enforcement is #451 item 6 — when it lands, this module is where the registry adopts it.

Display masking only — actually encrypting spp.registry.id.value (mixin + blind index) has search/dedup impact and is a separate change. spp_registry itself stays free of upward dependencies.

Test infrastructure note

No concrete model inherits spp.encrypted.field.mixin yet, so the wizard tests register a throwaway consumer via a vendored, Odoo-19-adapted FakeModelLoader (tests/fake_model_loader.py) — the released odoo-test-helper (2.1.3) targets pre-19 registry internals (MetaModel.module_to_models, Registry.setup_models) and cannot import on 19. Scoped to adding brand-new models; to be replaced when the helper supports Odoo 19. Legacy plaintext rows are fabricated with parameterized SQL, since ORM creates auto-encrypt.

Verification

  • ./spp t spp_pii_encryption47/47 (7 original mixin tests + wizard suite: end-to-end encrypt of legacy plaintext, blind-index creation, read() roundtrip, batch looping, per-record failure isolation, unreadable-model skip, dry-run immutability)
  • ./spp t spp_registry_encryption2/2 (get_view validates the inherited arch and asserts the widget on both tabs — this also proves Odoo 19 view validation accepts the widget's XML attributes)
  • ./spp lint green; module load logs clean (label-clash warning gone)
  • Manual UI smoke test pending (reason this is a draft): masked render + gated reveal + audit row + edit-through-widget in the editable list — the widget has not been browser-tested in a list context

Notes

  • README.rst/index.html regeneration will be applied from CI's printed diff after the first CI round (established workflow).
  • Follow-ups to file: hoot unit test for applyMask, tour test for reveal→audit, job-worker-based async migration for very large tables.

@gonzalesedwin1123

Copy link
Copy Markdown
Member Author

Reviewer note: read this together with #451 (deferred findings from the #232 adversarial review).

  • The spp_pii_encryption hardening: deferred design findings from the PR #232 adversarial review #451 gate ("items 1–5 before the first mixin adopter") is not tripped by this PR: no model adopts spp.encrypted.field.mixin here — the wizard is dormant until an adopter exists, and spp_registry_encryption does display masking only.
  • However spp_pii_encryption hardening: deferred design findings from the PR #232 adversarial review #451 items 6–7 apply to the masking wiring in this PR: the widget masks only in readonly display (entering the editable list cell shows plaintext without the reveal group or an audit row), reveal_group gates a client-side state rather than data access (the value reaches the browser via web_read regardless), and the ****-****-#### mask deliberately shows the last 4 characters. Whether this ships as documented UI de-emphasis, gets an edit-mode-masking widget change, or waits for server-side enforcement is an open design decision — flagged to Edwin. The module description will be aligned with whatever is decided before this leaves draft.

Base automatically changed from migrate-spp-data-classification to 19.0 August 25, 2026 06:00
…y-run/migrate)

Ported from openspp-modules, unblocked by spp_data_classification
(is_pii): the scan is driven by the classification registry.

Deliberate changes from the source:
- No in-app rollback and no plaintext backup table. The rollback relied
  on a skip_encryption context no mixin implements (it re-encrypted on
  write), and a persistent plaintext copy of the values being encrypted
  contradicts ADR-012 threat model ("backup exposure"). The Migrate
  confirm dialog now demands a database snapshot instead.
- Migration loops batches until each field is exhausted, with failed
  records excluded from the search domain so one broken record cannot
  loop forever; previously only the first batch was processed while the
  summary claimed completion.
- Scan reads the classification's stored model_name and tolerates
  AccessError per model (logged as skipped) — the encryption admin is
  deliberately not a system admin and cannot read ir.model records.
- batch_size validated positive; user-facing strings translated.

Tests register a concrete mixin consumer via a vendored Odoo-19
adaptation of odoo-test-helper's FakeModelLoader (the released helper
targets pre-19 registry internals); legacy plaintext rows are fabricated
with direct SQL since ORM creates auto-encrypt.

Module version 19.0.2.0.0 (clears the openspp-modules 19.0.1.0.0
baseline that included the wizard).
…d reveal

New applier module wiring the masked_char widget (spp_pii_encryption)
into the individual and group Identity tabs: spp.registry.id values
render as ••••-••••-1234, and revealing them requires
spp_data_classification.group_pii_full_access_admin — the PII access
group the RESTRICTED classification level points at — with every reveal
written to the PII access audit log.

Display masking only: encrypting the stored values (mixin on
spp.registry.id) is a separate change with search/dedup impact.
spp_registry itself stays free of upward dependencies.
@codecov

codecov Bot commented Aug 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.47853% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.66%. Comparing base (4e928a6) to head (fa7e3bb).

Files with missing lines Patch % Lines
...i_encryption/wizard/encryption_migration_wizard.py 94.40% 9 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             19.0     #452      +/-   ##
==========================================
- Coverage   76.72%   76.66%   -0.06%     
==========================================
  Files         634      631       -3     
  Lines       42625    42514     -111     
==========================================
- Hits        32703    32593     -110     
+ Misses       9922     9921       -1     
Flag Coverage Δ
spp_base_common 91.07% <ø> (ø)
spp_data_classification ?
spp_pii_encryption 95.33% <94.47%> (-0.49%) ⬇️
spp_programs 65.53% <ø> (ø)
spp_registry 87.79% <ø> (ø)
spp_security 69.56% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
spp_pii_encryption/__init__.py 100.00% <100.00%> (ø)
...p_pii_encryption/models/field_encryption_config.py 95.65% <ø> (ø)
spp_pii_encryption/wizard/__init__.py 100.00% <100.00%> (ø)
...i_encryption/wizard/encryption_migration_wizard.py 94.40% <94.40%> (ø)

... and 5 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…tion

spp_pii_encryption files applied verbatim from the CI pre-commit run
(32815132448). spp_registry_encryption files bootstrapped via the pinned
oca-gen hook in a pre-commit-managed env (CI cannot print a diff for
files that do not exist yet); if CI renders them differently, its next
printed diff is authoritative.
Per the widget-honesty findings in the spp_pii_encryption hardening
tracker (#451 items 6-7): the widget masks readonly display and audits
reveals through its control, but entering the editable cell shows the
value and the plaintext reaches the browser via the normal record read
either way. The description now says exactly that — the access boundary
remains record ACLs; the mask buys shoulder-surfing protection and an
audit trail for deliberate reveals. Mask keeps the last-4 pattern the
classification registry itself seeds for national IDs; a platform-wide
default-mask change belongs to #451 item 7.
Applied verbatim from CI pre-commit run 32817863234.
Two leaks found while browser-testing the first real wiring
(spp_registry_encryption, individual form Identity tab):

- The reveal toggle click bubbled to the list cell, so the "gate" button
  itself opened the row editor and exposed the plaintext input with no
  permission check and no audit entry. The click now stays on the toggle
  (t-on-click.stop); entering edit mode remains possible by clicking the
  cell directly, which is the documented de-emphasis behavior.
- The list renderer copies every char cell's formatted (raw) value into
  the cell's data-tooltip for truncated columns, so hovering a masked
  cell showed the full plaintext. masked_char columns now get no cell
  tooltip (ListRenderer.getCellTitle patch).

Verified in a live browser (headless Chrome against the dev stack):
masked render in the editable list, reveal refused without the PII
group (toast, no audit row), reveal + spp.pii.audit.log row with it,
edit-through-widget persists and re-masks on save; no console errors.
Applied verbatim from CI pre-commit run 32822423156.
@gonzalesedwin1123
gonzalesedwin1123 marked this pull request as ready for review August 25, 2026 07:59
@gonzalesedwin1123
gonzalesedwin1123 merged commit 38baa5c into 19.0 Aug 25, 2026
21 checks passed
@gonzalesedwin1123
gonzalesedwin1123 deleted the migrate-spp-pii-encryption-pr3 branch August 25, 2026 07:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant