security(cr): validate program access on assign-program detail - #338
Draft
gonzalesedwin1123 wants to merge 7 commits into
Draft
security(cr): validate program access on assign-program detail#338gonzalesedwin1123 wants to merge 7 commits into
gonzalesedwin1123 wants to merge 7 commits into
Conversation
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## 19.0-staging-sec-batch2 #338 +/- ##
===========================================================
+ Coverage 70.17% 70.21% +0.03%
===========================================================
Files 205 205
Lines 17751 17775 +24
===========================================================
+ Hits 12457 12480 +23
- Misses 5294 5295 +1
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
Member
Author
This was referenced Jul 24, 2026
The spp.cr.detail.assign_program program_id domain only constrains the UI, so
a CR user could RPC-write an arbitrary program id. The apply strategy runs on
self.sudo(), so program record rules and multi-company scope were bypassed at
validation, membership creation, and preview - allowing assignment to a
hidden/cross-company program and leaking its name.
Add @api.constrains('program_id') on the detail that, in the writing user's own
context, rejects a program the user cannot see (record rules) or that is outside
their company scope (explicit env.companies check, since the global company
ir.rule is not reliably applied to searches in the write context).
Bump 19.0.1.0.1 -> 19.0.1.0.3 (dodging #261's 19.0.1.0.2 on the same module) +
HISTORY. No migration: module is not in any released tag.
…angelog Applies CI's pinned oca-gen output verbatim for the new HISTORY fragment.
The keep-a-changelog '## <version> (date)' / '### Fixed' fragment made the oca-gen README generator crash (docutils 'Inconsistent title style: skip from level 2 to 4'). Switch to OCA flat '### <version>' entries (matching the module's DESCRIPTION heading ladder). README.rst reverted to base so CI's pinned generator regenerates it cleanly.
…TORY Applies CI's pinned oca-gen output verbatim for the OCA-style changelog.
- Correct the constraint docstring: the explicit company check is load-bearing, not defense-in-depth. Verified by experiment that the global multi-company ir.rule on spp.program is NOT applied to the search in this write/constraint context, so search-only would let a cross-company program through; drop the inaccurate 'area/registrant record rules' claim (spp.program has only the company rule). - Document in the apply strategy that program access is enforced at detail write time, so any future sudo writer of program_id must re-check. - Make the tests robust to in-flight PR #261's detail-ownership rule by using the cr_user's own partner as the CR registrant (matches #261's registrant branch); add a shared-program (company_id=False) allow test. Tests: 23 pass (was 22), run as a non-admin group_cr_user.
gonzalesedwin1123
force-pushed
the
security-cr-assign-program-access-bypass
branch
from
August 10, 2026 06:02
fbf04f6 to
1028928
Compare
) Review response: the write-time @api.constrains guards the input path, but the module ships in released tags (2026.07/2026.08) without it, so a program_id written before the constraint existed is never re-validated and the sudo apply strategy would trust the stored value — applying a registrant to a hidden/cross-company program on an upgraded DB. Add _check_program_access_at_apply() to the strategy validate(), bound to the change-request requester's (create_uid) company scope — the stable identity whose authority the assignment rides on, not the sudo/manager apply-time actor. Closes the released-DB residual permanently and also covers any future sudo prefill/import that sets program_id without firing constraints. No-op in single-company deployments. Keeps the @api.constrains as write-time defense in depth. Test plants a cross-company program_id + company-A requester via direct SQL (bypassing the constraint, simulating a pre-upgrade record) and asserts apply rejects it; positive cases for in-scope and shared programs.
gonzalesedwin1123
changed the base branch from
19.0
to
19.0-staging-sec-batch2
August 14, 2026 09:57
…338) Follow-up to the apply-sink guard: preview() also runs under sudo (via the preview snapshot / HTML), so a pre-existing out-of-scope program_id would still leak the cross-company program's display_name there even though apply now rejects it. Extract _program_accessible_to_requester() and have preview() redact the name (non-throwing) when the requester cannot access the program. Tests: preview redacts an inaccessible program, still shows an accessible one.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The
spp_cr_type_assign_programapply strategy validated a selected program'sexistence,
state, target type, and uniqueness — but not the user's accessto the program or its company. The detail's
program_iddomainonlyconstrains the UI, so an authenticated CR user with detail write access could
submit a raw ORM/RPC
write({'program_id': <arbitrary id>}). On apply, core CRcode runs the strategy on
self.sudo()(spp.change.request._do_apply), soprogram record rules and multi-company scope were bypassed during validation and
membership creation — assigning the registrant to a hidden/cross-company
program — and preview generation leaked that program's display name through the
same sudo path.
Fix
Add
@api.constrains('program_id')tospp.cr.detail.assign_programthat, inthe writing user's own context, rejects a program they cannot access:
search()must return the program (enforces any recordrule on
spp.program, and rejects a stale/deleted id);company_idmust be withinself.env.companies. This explicit check is load-bearing, not justdefense-in-depth: verified by experiment that the global multi-company
ir.ruleonspp.programis not applied to the search in thiswrite/constraint context, so
searchalone would let a cross-company programthrough.
This guards the only sender-reachable input path to
program_id; the sudoapply/preview then only ever consume an already-validated, accessible program.
The apply strategy is annotated to make explicit that access is enforced at
detail-write time, so any future code that sets
program_idunder sudo mustperform its own access check.
Design decision (confirmed with owner)
Defense in depth: (1) an
@api.constrainsblocks storing an inaccessibleprogram_idat the source (write-time, in the user's own context), and (2) thesudo apply strategy re-asserts program access at the sink, bound to the change
request requester's company scope, before creating the membership.
Correction (2026-08-14): an earlier revision claimed the module is
"unreleased so no pre-existing poisoned records exist" and shipped the
constraint alone. That premise was false — the vulnerable module (v1.0.1,
no constraint) ships in released tags
2026.07and2026.08. Because@api.constrainsfires only on write, aprogram_idwritten before theconstraint existed is never re-validated, and the sudo apply would trust it.
The apply-time sink re-check (2) closes that residual permanently, and also
covers any future path that sets
program_idunder sudo without triggeringconstraints (import, prefill). No schema migration is required (constraint +
runtime check only); a poisoned pre-existing record simply fails at apply.
Testing
./spp t spp_cr_type_assign_program→ 26 passed, 0 failed, 0 errors (19existing + 4 new), run as a non-admin
group_cr_user.detail pointing at a company-B program; is allowed to select a company-A
program; and is allowed to select a company-shared (
company_id = False)program.
Staff review
Two independent adversarial staff reviews were run. Verdict: ship-with-nits, no
open security hole. Findings addressed in-branch:
proven by removing it and watching the reject tests fail).
user's own partner as registrant) and added the shared-program allow case.
Consciously left (low, non-security): an archived program is reported as "no
access" rather than "archived" (apply re-validates
stateregardless); and auser lacking any programs-read group gets
AccessErrorrather than the friendlyValidationError(pre-existing — the feature already requires a programs group).Notes for reviewers
cr-detail-record-rules) also touches thismodule — it adds parent-CR-ownership/area record rules to the detail
(which detail records you may write). This PR is complementary: it governs
which program you may target on a detail you legitimately own. Both bump
the manifest/HISTORY/README, so a version/changelog collision is expected;
this PR uses
19.0.1.0.3(higher than both the19.0base19.0.1.0.1andsecurity(cr): add record rules to CR detail models (ownership + area) #261's bump), so whichever merges second needs a trivial re-merge.
2026.07/2026.08); pre-existing out-of-scopeprogram_idrecords are caught at apply by the sink re-check rather than by a data migration.Draft: awaiting human security review before undraft/merge.
Merge order (CR security cluster)
Per the cross-PR interaction analysis (2026-07-24,
internal/plans/security-prs-interaction-analysis.md), hard order: #264 → #261 → #338 (this PR) → #343.spp_programs.group_programs_viewertouser_a,user_b, andvalidatorinspp_cr_type_assign_program/tests/test_detail_security.py. Reason: this PR's_check_program_accessconstraint runs aspp.programsearch()in the writing user's context, and security(cr): add record rules to CR detail models (ownership + area) #261's fixture users hold onlygroup_cr_user, which grants nospp.programread ACL — without the fix, 4 of 5 tests in that file fail on the combined tree. This order makes the failure (and the fix) surface in this PR's own CI.program_idremains repointable post-submit; approval-fidelity gap, fix via security(cr): route and apply the same single field for dynamic approvals #264's_protected_content_fields).