diff --git a/spp_cr_type_assign_program/README.rst b/spp_cr_type_assign_program/README.rst index 6a49d539f..4d7bd12e3 100644 --- a/spp_cr_type_assign_program/README.rst +++ b/spp_cr_type_assign_program/README.rst @@ -91,11 +91,21 @@ Dependencies Changelog ========= -19.0.1.0.0 (2026-05-04) ------------------------ - -Added -~~~~~ +19.0.1.0.3 +~~~~~~~~~~ + +- fix(security): validate server-side that the user selecting a program + on ``spp.cr.detail.assign_program`` can actually access it. The + ``program_id`` domain only constrained the UI, so a raw RPC write + could target a hidden or cross-company program; on apply the strategy + runs under ``sudo``, which would assign the membership and leak the + program name via preview while bypassing program record rules and + multi-company scope. An ``@api.constrains`` now rejects a program the + writing user cannot see (record rules) or that is outside their + company scope. + +19.0.1.0.0 +~~~~~~~~~~ - New module ``spp_cr_type_assign_program`` with the ``assign_program`` change request type. diff --git a/spp_cr_type_assign_program/__manifest__.py b/spp_cr_type_assign_program/__manifest__.py index f81be529e..5b66fe5f9 100644 --- a/spp_cr_type_assign_program/__manifest__.py +++ b/spp_cr_type_assign_program/__manifest__.py @@ -1,6 +1,6 @@ { "name": "OpenSPP CR Type - Assign to Program", - "version": "19.0.1.0.1", + "version": "19.0.1.0.3", "sequence": 53, "category": "OpenSPP", "summary": "Change request type for assigning a registrant to a program", diff --git a/spp_cr_type_assign_program/details/assign_program.py b/spp_cr_type_assign_program/details/assign_program.py index da30dade5..c9af590ae 100644 --- a/spp_cr_type_assign_program/details/assign_program.py +++ b/spp_cr_type_assign_program/details/assign_program.py @@ -1,4 +1,5 @@ -from odoo import api, fields, models +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError class SPPCRDetailAssignProgram(models.Model): @@ -35,6 +36,40 @@ class SPPCRDetailAssignProgram(models.Model): readonly=True, ) + @api.constrains("program_id") + def _check_program_access(self): + """Reject a program the selecting user cannot access. + + The `program_id` domain only constrains the UI; a raw ORM/RPC write can + point it at an arbitrary program. Since the apply strategy runs under + `sudo` (spp.change.request._do_apply), an inaccessible program would + otherwise be assigned - and its name leaked via preview - bypassing + program record rules and multi-company scope. Enforce access here, in + the writing user's own context, so the stored value can only ever be a + program that user may target. + + Two checks, because neither alone is sufficient: + - `search()` requires the program to be visible to the user, enforcing + any record rule on `spp.program` (and rejecting a stale/deleted id). + - an explicit `company_id in env.companies` guard enforces multi-company + scope directly. This is load-bearing, not mere defense in depth: the + global multi-company `ir.rule` on `spp.program` is NOT reliably + applied to the search in this write/constraint context (verified by + test - a company-A user's search still returns a company-B program), + so relying on `search()` alone would let a cross-company program + through. The explicit check rejects it deterministically. + """ + for rec in self: + program = rec.program_id + if not program: + continue + # `or` short-circuits: if the record is not visible, program.company_id + # is not read (avoids an AccessError on a rule-hidden record). + if not self.env["spp.program"].search([("id", "=", program.id)]) or ( + program.company_id and program.company_id not in self.env.companies + ): + raise ValidationError(_("You do not have access to the selected program.")) + @api.depends("registrant_id", "registrant_id.is_group") def _compute_registrant_target_type(self): for rec in self: diff --git a/spp_cr_type_assign_program/readme/HISTORY.md b/spp_cr_type_assign_program/readme/HISTORY.md index 74862b003..7639bf892 100644 --- a/spp_cr_type_assign_program/readme/HISTORY.md +++ b/spp_cr_type_assign_program/readme/HISTORY.md @@ -1,6 +1,15 @@ -## 19.0.1.0.0 (2026-05-04) +### 19.0.1.0.3 -### Added +- fix(security): validate server-side that the user selecting a program on + `spp.cr.detail.assign_program` can actually access it. The `program_id` + domain only constrained the UI, so a raw RPC write could target a hidden or + cross-company program; on apply the strategy runs under `sudo`, which would + assign the membership and leak the program name via preview while bypassing + program record rules and multi-company scope. An `@api.constrains` now rejects + a program the writing user cannot see (record rules) or that is outside their + company scope. + +### 19.0.1.0.0 - New module `spp_cr_type_assign_program` with the `assign_program` change request type. diff --git a/spp_cr_type_assign_program/static/description/index.html b/spp_cr_type_assign_program/static/description/index.html index aa6fd572c..8fa8f8ffe 100644 --- a/spp_cr_type_assign_program/static/description/index.html +++ b/spp_cr_type_assign_program/static/description/index.html @@ -446,21 +446,29 @@

Dependencies

Table of contents

Changelog

-
-

19.0.1.0.0 (2026-05-04)

+
+

19.0.1.0.3

+
-
-

Added

+
+

19.0.1.0.0