Skip to content

security(spp_change_request_v2): enforce manager authorization on CR apply (server-side) - #365

Draft
gonzalesedwin1123 wants to merge 5 commits into
19.0from
security-cr-apply-manager-authz
Draft

security(spp_change_request_v2): enforce manager authorization on CR apply (server-side)#365
gonzalesedwin1123 wants to merge 5 commits into
19.0from
security-cr-apply-manager-authz

Conversation

@gonzalesedwin1123

@gonzalesedwin1123 gonzalesedwin1123 commented Jul 27, 2026

Copy link
Copy Markdown
Member

Problem

change_request.py::action_apply() runs the apply strategy under self.sudo() (via _do_apply), so the
strategy executes as superuser and can write models CR roles cannot — notably spp.group.membership,
which is intentionally read-only for CR roles. The group_cr_manager restriction, however, lived only on
the review button
(stage_review_form.xml). Odoo object methods are callable over RPC, so button groups=
is not a server-side authorization boundary.

group_cr_user has write on spp.change.request, so a CR user could call action_apply() directly over RPC
on an approved change request they own and drive superuser membership writes (e.g. remove_member,
transfer_member create/end membership rows). Severity: high. (The report also notes approval_state is
an unprotected field on the shared approval mixin — filed separately; see below.)

Fix

  • Gate the public action_apply() server-side: env.su or has_group("…group_cr_manager"), else AccessError.
  • Move the apply mechanism to an internal, non-RPC _apply_change_request() (underscore methods are not
    callable via call_kw), so the authorization boundary lives on the public method.
  • _on_approve() (auto-apply-on-approve, default on) now calls _apply_change_request() directly — approval
    is validator-gated and already authorizes the apply, so this does not break the primary flow where a
    validator's approval auto-applies. Superuser/sudo callers (demo generators) are unaffected via env.su.
  • _do_apply()'s sudo() is unchanged (membership writes are a system action by design); it is now only
    reachable post-gate or post-approval.
  • No schema/ACL/data change → no migration.

Tests

Test-first (red → green). New tests/test_apply_authorization.py:

  • test_cr_user_cannot_apply_over_rpc — a group_cr_user, on their own approved remove_member CR (owned
    so the CR ownership record rule doesn't mask the check), calling action_apply() raises AccessError and
    the membership is untouched. Failed before the fix (AssertionError: AccessError not raised — it applied
    as superuser).
  • test_manager_can_apply — a group_cr_manager can apply (regression).
  • test_validator_cannot_apply_directly — a group_cr_validator is also blocked from calling action_apply() directly (documents the manager-only boundary).
  • test_auto_apply_on_approve_runs_for_non_manager_approver — a validator's _on_approve still auto-applies
    (guards the split from breaking auto-apply).

./spp t spp_change_request_v20 failed of 338. No existing tests changed.

Staff review

An adversarial staff review (verdict ship) confirmed action_apply is the only ungated public path to
the sudo-apply sink (batch/conflict wizards reach apply only via approval-gated action_approve), that
_apply_change_request is not RPC-callable, and that all existing apply tests pass via the superuser test
context. Its one nit — asserting a validator is also blocked — was folded in. It also confirmed the deferred
approval_state weakness (#369) does not enable an apply bypass here, since a group_cr_user cannot
self-approve through the workflow.

Scope / notes

…ia sudo path

A group_cr_user, on their OWN approved remove_member CR (owning it so the
ownership record rule doesn't mask the check), can call action_apply() over
RPC and drive the sudoed strategy to end membership. Test asserts AccessError
+ untouched membership (fails before the fix: 'AccessError not raised'), that
a manager can apply, and that validator-driven auto-apply-on-approve still
applies.
…apply

action_apply() runs the apply strategy under sudo (writing spp.group.membership
etc. that CR roles cannot), but the group_cr_manager restriction existed only
on the review button. Odoo object methods are RPC-callable, so a group_cr_user
could invoke action_apply() directly on an approved CR (one they own) and drive
superuser membership writes via strategies like remove_member/transfer_member.

Gate the public action_apply() with a server-side check (env.su or
group_cr_manager) and move the apply mechanism to an internal, non-RPC
_apply_change_request(). Auto-apply-on-approve (_on_approve) now calls the
internal mechanism directly so validator-driven approvals still apply, and
sudo/demo callers are unaffected.
…z fix

19.0.3.0.0 -> 19.0.3.0.4 (dodges #264 .0.1 / #261 .0.2 / #353 .0.3 on this
module). README.rst / index.html to be regenerated from CI.
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 70.32%. Comparing base (1caf794) to head (6c3eea1).
⚠️ Report is 3 commits behind head on 19.0.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             19.0     #365      +/-   ##
==========================================
- Coverage   74.28%   70.32%   -3.97%     
==========================================
  Files         372      205     -167     
  Lines       25385    17892    -7493     
==========================================
- Hits        18857    12582    -6275     
+ Misses       6528     5310    -1218     
Flag Coverage Δ
spp_analytics ?
spp_api_v2_change_request 66.53% <ø> (?)
spp_api_v2_cycles ?
spp_api_v2_entitlements ?
spp_api_v2_gis ?
spp_api_v2_programs ?
spp_api_v2_simulation ?
spp_audit_programs ?
spp_base_common 91.07% <ø> (+0.80%) ⬆️
spp_case_demo ?
spp_case_entitlements ?
spp_case_programs ?
spp_change_request_v2 77.94% <100.00%> (?)
spp_cr_type_assign_program 92.07% <ø> (+0.90%) ⬆️
spp_dci_compliance ?
spp_dci_demo 94.28% <ø> (+0.88%) ⬆️
spp_dci_server_social ?
spp_demo ?
spp_demo_phl_luzon ?
spp_drims ?
spp_drims_sl ?
spp_drims_sl_demo ?
spp_farmer_registry_cr 61.24% <ø> (?)
spp_farmer_registry_demo 61.06% <ø> (+0.04%) ⬆️
spp_gis_report ?
spp_grm_demo ?
spp_indicator ?
spp_indicator_studio ?
spp_metric_service ?
spp_mis_demo_v2 70.99% <ø> (+0.03%) ⬆️
spp_programs 65.27% <ø> (+<0.01%) ⬆️
spp_registry 86.94% <ø> (+0.10%) ⬆️
spp_security 69.56% <ø> (+2.89%) ⬆️
spp_simulation ?
spp_starter_sp_mis 86.66% <ø> (?)
spp_studio_change_requests 84.85% <ø> (?)

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

Files with missing lines Coverage Δ
spp_change_request_v2/models/change_request.py 84.21% <100.00%> (ø)

... and 334 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.

… for CR apply authz

README.rst / index.html regenerated from CI's pinned oca-gen output (19.0.3.0.4);
test file reflowed to CI's ruff-format line length (local hook env differed).
@gonzalesedwin1123

Copy link
Copy Markdown
Member Author

Follow-up filed: #369approval_state on the shared spp.approval.mixin has no field-level protection, so group_cr_user (and other mixin-consumer writers) can write({'approval_state': 'approved'}) directly, bypassing the approval workflow. Cross-cutting (6 consumer models); scoped out of this PR, which already closes the dangerous sudo-apply sink.

…direct apply

From staff review: document the manager-only boundary explicitly — a
group_cr_validator (not a manager) calling action_apply directly is denied;
validators cause an apply only by approving (auto-apply).
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