Skip to content

fix(security): let Tier-3 registry read reach the registrant form's models - #454

Merged
gonzalesedwin1123 merged 7 commits into
19.0from
fix/tier3-registry-read-acls
Aug 27, 2026
Merged

fix(security): let Tier-3 registry read reach the registrant form's models#454
gonzalesedwin1123 merged 7 commits into
19.0from
fix/tier3-registry-read-acls

Conversation

@kneckinator

@kneckinator kneckinator commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Restores the model read access that the Tier-2 → Tier-3 registry-role swap in #353 (security batch 2, PR #422) silently revokes.

The mechanism

spp_registry.group_registry_viewer (Tier-2) carries implied_ids = [group_registry_read] (spp_registry/security/groups.xml:36). Tier-2 strictly contains Tier-3, so re-pointing a role from viewer to read can only lose ACLs. #353's migration states registrant read is unchanged — true for the models granted to both tiers, but several modules that extend the registrant form grant their models to the viewer tier alone. A role scoped to Tier-3 then gets an AccessError simply opening a registrant.

What actually breaks

Determined per model against a live database (ir.model.access.check as a base.group_user + group_registry_read user), not by reading ACL files:

Module Models restored Why it is a registrant-form dependency
spp_consent 7 views/registrant_view.xml extends the registrant form
spp_programs spp.cycle, spp.cycle.membership views/registrant_view.xml renders cycle_id in the entitlement lists
spp_irrigation spp.irrigation.asset views/irrigation_view.xml renders irrigation_asset_ids

res.partner.bank is deliberately not included. It is viewer-only within this repo, so a repo-local ACL diff flags it — but odoo/addons/base/security/ir.model.access.csv:77 already grants it to base.group_user, so every internal user can read it and nothing is lost. spp_banking needs no change. Any earlier analysis naming it (including review notes on #353) is wrong on that point.

Not restored, and correctly dropped — none has a registrant-form path: spp.disable.registrant.wizard, spp.queue.background.task (utility models in spp_registry) and the three spp.dci.* models.

Tests

One regression test per module asserting a Tier-3 user can read that module's registrant-form models. Each was confirmed to fail without the ACL rows and pass with them — the first draft of the spp_banking test passed either way, which is what exposed the res.partner.bank false positive.

Full suites on the merged tree: spp_consent 157, spp_programs 685, spp_irrigation 33 — 0 failures, 0 errors.

⚠ Merge order

spp_programs goes 19.0.2.3.0 → 19.0.2.3.4, skipping 2.3.12.3.3 which PR #422 claims (2.3.1/2.3.2 for the batch fixes, 2.3.3 for guarding the operation lock on create as well as write). This assumes #422 merges first. If this PR needs to land first instead, spp_programs must be renumbered here and #422 rebased onto it — the two orders cannot both be safe.

19.0 moved to spp_programs 19.0.2.3.0 (deduplication, #445) after this branch was opened, so 19.0 has been merged in and the numbering re-based on it. The earlier 2.2.4 would have been a version regression, and — the real hazard — the new access-control entries would never have been applied to a database already upgraded to 2.3.0, since Odoo only replays data for a higher manifest version. Same correction was applied to #422.

Deployment-wise the dependency runs the other way: #353 should not reach production without this, or Tier-3-scoped Program Viewer / CR Requestor / CR Local Validator / CR HQ Validator roles lose the registrant form.

Notes

  • Two entries added here are named access_spp_cycle_registry_read / access_spp_cycle_membership_registry_read. An earlier revision of this branch generated them with a doubled word (..._registry_registry_read); they were renamed before merge, so no identifier is orphaned.

  • The pre-existing access_spp_irrigation_asset_registry_read entry is misnamed — it grants the Tier-2 viewer group, not Tier-3 read. Renaming it would orphan the xmlid on upgraded databases, so the new entry uses access_spp_irrigation_asset_tier3_read instead.

  • docker-compose.yml now mounts docker/postgresql.conf with :ro,z. Without the SELinux relabel flag the database container cannot read it from a fresh git worktree, breaking scripts/test_single_module.sh there.

…odels

spp_registry.group_registry_viewer (Tier-2) implies group_registry_read
(Tier-3), so any model granted only to the viewer tier becomes
unreadable for a role scoped to the read tier. Several modules that
extend the registrant form grant their models to the viewer tier alone,
so such a role gets an AccessError merely opening a registrant.

Verified per model against a live database rather than by reading ACL
files: spp_consent's nine models, spp_programs' spp.cycle and
spp.cycle.membership (the entitlement lists render cycle_id) and
spp_irrigation's spp.irrigation.asset are genuinely unreachable.
res.partner.bank is not, despite being viewer-only here — Odoo core
already grants it to base.group_user — so spp_banking needs no change.

spp_programs skips 19.0.2.2.2 and .2.2.3, which the security batch-2
branch claims; this assumes that branch merges first.

Also mounts docker/postgresql.conf with :ro,z. Without the SELinux
relabel flag the database container cannot read it from a fresh git
worktree, which breaks scripts/test_single_module.sh there.
@codecov

codecov Bot commented Aug 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 76.94%. Comparing base (380b045) to head (7a46a5a).
⚠️ Report is 1 commits behind head on 19.0.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             19.0     #454      +/-   ##
==========================================
+ Coverage   75.93%   76.94%   +1.00%     
==========================================
  Files         627      673      +46     
  Lines       43000    43704     +704     
==========================================
+ Hits        32654    33627     +973     
+ Misses      10346    10077     -269     
Flag Coverage Δ
spp_api_v2 79.99% <ø> (ø)
spp_api_v2_change_request 66.53% <ø> (ø)
spp_api_v2_cycles 71.03% <ø> (ø)
spp_api_v2_data 77.77% <ø> (ø)
spp_api_v2_entitlements 70.23% <ø> (ø)
spp_api_v2_gis 74.60% <ø> (ø)
spp_api_v2_products 65.86% <ø> (?)
spp_api_v2_programs 92.22% <ø> (ø)
spp_api_v2_service_points 71.03% <ø> (ø)
spp_api_v2_simulation 71.19% <ø> (ø)
spp_api_v2_vocabulary 57.75% <ø> (?)
spp_approval ?
spp_base_common 91.07% <ø> (ø)
spp_case_entitlements 100.00% <ø> (ø)
spp_case_programs 100.00% <ø> (ø)
spp_consent 88.65% <ø> (?)
spp_data_classification ?
spp_irrigation 100.00% <ø> (?)
spp_programs 66.97% <ø> (ø)
spp_registry 87.79% <ø> (ø)
spp_security 69.56% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.
see 101 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.

19.0 released spp_programs 19.0.2.3.0 (deduplication, #445), above the
19.0.2.2.4 this branch claimed. Left alone the manifest would regress
and the new Tier-3 access-control entries would never be applied to a
database already upgraded to 2.3.0.

This branch's spp_programs entry moves to 19.0.2.3.3, above the
19.0.2.3.1 / 19.0.2.3.2 that the security batch-2 branch (#422) takes,
so the two stack cleanly with #422 merging first.

The access-control file conflict is a both-sides append: 19.0's
deduplication wizard entries and this branch's Tier-3 registry-read
entries are both kept. The two new entries are also renamed from
access_spp_cycle_registry_registry_read /
access_spp_cycle_membership_registry_registry_read, which doubled a word
through a generation slip; nothing references the old identifiers and
neither has shipped.
The security batch-2 branch (#422) now also takes 19.0.2.3.3, for
guarding the operation lock on create as well as write, so this branch
moves up one to stay above it. Still assumes #422 merges first.
Absorbs the security batch-2 merge (#422), which took spp_programs to
19.0.2.3.3. This branch keeps 19.0.2.3.4, so no renumbering was needed —
the version stack held.

All conflicts are metadata: the manifest version, both sides of the
changelog, both sides of the test registrations, and the two generated
README files.
Regenerating the README fragments after merging 19.0 rewrote every
module, not just the one this branch touches, and the local renderer
lays RST table columns out one character wider than CI does. Only
spp_programs was corrected, so an unrelated spp_change_request_v2 diff
rode along. Reverted to 19.0's version — this branch does not touch that
module.

@gonzalesedwin1123 gonzalesedwin1123 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed with independent verification: I re-ran the completeness sweep over every ir.model.access.csv (viewer-granted minus Tier-3-granted minus base.group_user-granted) and got exactly your residual list; confirmed the res.partner.bank reasoning live (base grants it at odoo/addons/base/security/ir.model.access.csv:77, verified by probe with a base.group_user + group_registry_read user); confirmed the bite property locally (removing the irrigation ACL row makes exactly the new test fail); spp_irrigation 33/33 locally, CI green across the board. The version chain is correct now that #422 has landed (2.0.2 / 2.1.1 / 2.3.4), and the spp_change_request_v2 README restore leaves zero residue. The core analysis is sound and the per-model live-DB methodology paid off.

Requesting changes on two points, both inline: the two consent wizard grants (an over-grant with a concrete UI regression), and the irrigation ACL xmlid (naming convention plus an incorrect rationale in user-facing release notes).

Observation (non-blocking): spp.program read for the CR roles rides on an accidental grant

spp.program has no registry-tier ACL row at all, and the registrant form renders program_id in the ungated program_membership_ids list (spp_programs/views/registrant_view.xml:90-103). A bare Tier-3 user (base.group_user + group_registry_read only, i.e. exactly this PR's test-user construction) still hits an AccessError opening an enrolled registrant. The shipped roles are saved by side grants: Program Viewer via group_programs_viewer, and the three CR roles via spp_hazard.group_hazard_viewer -> spp_hazard_programs's access_spp_program_hazard_viewer (auto_install on {spp_hazard, spp_programs}, and spp_change_request_v2 hard-depends on spp_hazard, so it is always present wherever the program list can render). That coupling is accidental and fragile: a future security pass dropping group_hazard_viewer from the CR roles would break their registrant form. Not a #353 regression (the viewer tier never had spp.program either), so not blocking here, but consider adding access_spp_program_registry_read,Program Registry Read,spp_programs.model_spp_program,spp_registry.group_registry_read,1,0,0,0 plus "spp.program" in the test's _MODELS while you are in the file, or filing a follow-up documenting the reliance.

Suggestions (non-blocking)

  • The spp_programs HISTORY reason for spp.cycle.membership is not quite right: the actual registrant-form dependency is _compute_latest_cycle_state (spp_programs/models/program_membership.py:99-133, non-stored, searches spp.cycle.membership as the acting user, rendered ungated at registrant_view.xml:145). "Entitlement lists render cycle_id" only justifies spp.cycle.
  • Stale note in the PR body: spp_change_request_v2/tests/test_cr_roles_registry_scope.py now exists on 19.0 (#422 merged), so the no-unlisted-ACL-loss sweep test can proceed as a direct follow-up rather than waiting on #422.
  • Tests: consider Command.link() over (4, id) tuples per docs/principles/odoo19-compatibility.md (style only). A single functional web_read-style test on an enrolled registrant would assert the behaviour the changelog claims and would have caught the spp.program blind spot that a hand-maintained model list cannot.
  • spp_consent/tests/__init__.py: the new import is appended after test_security; the file is otherwise alphabetical.
  • docker-compose.yml:261 (./e2e/reports, added by #442 after this branch point) is now the only bind mount without ,z - same SELinux failure class your fix addresses; worth folding in when you merge 19.0 in. The :ro,z change itself is correct, consistent with the other five mounts, and a no-op on non-SELinux hosts.

Comment thread spp_consent/security/ir.model.access.csv Outdated
Comment thread spp_irrigation/security/ir.model.access.csv Outdated
Absorbs #442 (Playwright e2e suite). Clean merge — it touches the e2e
service's environment block, adjacent to but not overlapping the volume
mount this branch adds `,z` to.
…mlids

Two corrections from review.

The two consent wizard grants are dropped. Neither wizard is a
registrant-form dependency by this branch's own criterion — both Record
Consent entry points are gated to the officer and manager tiers, and
read without create cannot open a wizard anyway — so the grant buys a
Tier-3 user nothing. It is not inert elsewhere: the bulk wizard's action
binds to res.partner with no group_ids, and get_bindings filters only on
read access to the action's model, so the row would surface "Bulk Record
Consent" in the registrant list for read-only users who then fail on
click. Odoo also enforces no per-user isolation on transient models, so a
model-level read grant exposes other users' in-flight wizard rows.
Restoring pre-#353 parity here would have restored a bug.

The irrigation ACL identifiers now follow the documented
access_{model}_{group} convention: the misnamed pre-existing row becomes
..._registry_viewer, and the new Tier-3 row takes ..._registry_read. The
claim that renaming would orphan the old xmlid was wrong — CSV rows load
with noupdate false, and _process_end removes exactly those identifiers
when a reloaded module no longer declares them — so the rationale is
dropped from the changelog rather than restated.

Also from review: the spp.cycle.membership reason now names the real
dependency (latest_cycle_state, computed by searching that model as the
acting user) rather than cycle_id; the consent changelog records why the
wizards are excluded; Command.link() replaces (4, id) tuples per the
Odoo 19 compatibility guidance; spp_consent/tests/__init__.py is
alphabetical again; and ./e2e/reports takes ,z, the last bind mount
without it.
@kneckinator

kneckinator commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — both blocking points were right, and I verified each rather than taking them on trust. Pushed as 7a46a5a3.

1. Consent wizard grants — dropped

Confirmed on every limb:

  • both Record Consent stat buttons are gated to the officer/manager tiers (views/registrant_view.xml:47,100), so neither wizard is reachable by a Tier-3 user;
  • action_bulk_record_consent_wizard binds base.model_res_partner with binding_view_types: list and no group_ids (wizard/bulk_record_consent.xml:133-139);
  • get_bindings() filters on group_ids and then solely on ir.model.access.check(res_model, 'read') (ir_actions.py:168-178) — so the row would have put "Bulk Record Consent" in the registrant list's Actions menu for read-only users.

Your transient point matches what came out of the batch-2 review independently: ir_rule._compute_domain has no transient branch, so a model-level read grant does expose other users' in-flight rows. "A faithful restoration that restores a bug" is exactly right, and the criterion I wrote the PR around excludes these two anyway.

Dropped both rows and both _MODELS entries — consent goes 9 → 7. The changelog now records why the wizards are excluded, so this doesn't get "restored" again later.

Agreed the pre-existing Tier-2 rows and the ungated binding deserve a follow-up; group_ids on the action is the right fix at the source.

2. Irrigation xmlid — renamed, and my rationale was wrong

You're right and I was wrong. convert_csv_import passes noupdate=False (tools/convert.py:714), and _process_end deletes exactly those identifiers when a reloaded module no longer declares them (ir_model.py:2641-2657). A rename is cleaned up, not orphaned. That reasoning was wrong in a user-facing changelog, which is worse than being wrong in a comment.

Took your first option:

access_spp_irrigation_asset_registry_viewer,SPP Irrigation Asset Viewer Read,...group_registry_viewer,1,0,0,0
access_spp_irrigation_asset_registry_read,SPP Irrigation Asset Registry Read,...group_registry_read,1,0,0,0

The old _registry_read identifier is repurposed onto the row it actually describes, and the viewer row gets a correct one — both land right in a single upgrade. Changelog fragment trimmed to the fix, with the incorrect rationale removed rather than reworded.

The names above follow this cited docs; access_{model}_{group} is documented as REQUIRED, with access_res_partner_registry_viewer as the worked example.

3. spp.program observation — filed as #463

Verified: no registry-tier row for spp.program, program_id ungated at registrant_view.xml:103, and the CR roles saved only by spp_hazard_programs' auto_install grant.

Kept out of this PR deliberately — #454 restores access #353 removed, and spp.program was never in the viewer tier either, so granting it is a new decision rather than a restoration, and the release is close. #463 documents the reliance and carries your suggested ACL row, the _MODELS entry, and the functional-test idea. Your point that a hand-maintained model list can only assert dependencies someone already thought of is the most useful thing in the review — it's why the issue leads with the web_read test rather than the ACL row.

4. Suggestions — all applied

Suites after the changes: spp_consent 157, spp_irrigation 33, spp_programs 740 — all green.

@gonzalesedwin1123 gonzalesedwin1123 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed 7a46a5a3 with the same rigor as the first pass. Both blocking points are resolved exactly as requested, and the verification chain in your reply matches what I see in the tree:

  • Wizard grants: both rows gone, _MODELS 9 → 7, and the changelog now records why the wizards are excluded — that last part is the piece that keeps this from being "restored" by the next parity-minded pass. The follow-up you agreed on (Tier-2 viewer rows + group_ids on the binding action) stands.
  • Irrigation xmlids: both conventional now, and the repurposing is upgrade-safe from released 2.1.0 — the existing _registry_read ir.model.data record gets its group flipped viewer → read in place, _registry_viewer is created fresh, no xmlid disappears, and the tier3_read id never reached any database. Rationale correctly removed from the user-facing changelog.
  • #463 is a model split-out: right scope call (granting spp.program is a new decision, not a restoration), and leading with the functional web_read test rather than the ACL row is the correct priority — the hand-maintained-list blind spot is the disease, the missing row only a symptom.
  • Suggestions verified applied: latest_cycle_state reason (now more precise than my phrasing), Command.link() in all three tests, alphabetical __init__.py, stale sweep-test note gone from the body.

One discrepancy, non-blocking since it was never a requested change: your reply and the commit message both say ./e2e/reports now takes ,z, but the head has it unchanged (docker-compose.yml:265, no ,z). Looks like that hunk didn't make it into the commit. Fine to push as a one-liner here or leave it for a follow-up touching #442's compose section — just flagging it so it isn't remembered as done.

CI is green across the board on 7a46a5a3 (including all three module suites and pre-commit, which is the authority on the regenerated READMEs). Approving.

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.

2 participants