diff --git a/spp_mis_demo_v2/tests/test_access_control.py b/spp_mis_demo_v2/tests/test_access_control.py index fc31ed59c..ed9bbf5b6 100644 --- a/spp_mis_demo_v2/tests/test_access_control.py +++ b/spp_mis_demo_v2/tests/test_access_control.py @@ -82,6 +82,13 @@ def setUpClass(cls): """Set up test users with different roles.""" super().setUpClass() + # These tests measure the role-based ACLs, so pin the SP-MIS registry + # access-control switch off: this bundle ships it on, and it withholds + # registrant create/write/unlink from everyone but admins regardless of + # role. It used to be a no-op on the server, which is why these tests + # never had to say so (OP#1142). + cls.env["ir.config_parameter"].sudo().set_param("spp_starter.registry_admin_only_crud", "False") + # Check which modules are installed cls.grm_installed = bool(_safe_ref(cls.env, "spp_grm.group_grm_viewer")) cls.case_installed = bool(_safe_ref(cls.env, "spp_case_base.group_case_viewer")) diff --git a/spp_starter_sp_mis/README.rst b/spp_starter_sp_mis/README.rst index dd132e795..f17a89b0f 100644 --- a/spp_starter_sp_mis/README.rst +++ b/spp_starter_sp_mis/README.rst @@ -25,8 +25,8 @@ OpenSPP Starter: SP-MIS Starter bundle for Social Protection Management Information System (SP-MIS) deployments. Extends ``spp_starter_social_registry`` with program management, approval workflows, and service delivery -capabilities. Adds optional client-side registry access control to -restrict registrant editing to administrators. +capabilities. Adds optional registry access control, enforced on the +server, to restrict registrant editing to administrators. Key Capabilities ~~~~~~~~~~~~~~~~ @@ -35,11 +35,12 @@ Key Capabilities program management modules in a single deployment - **Starter Type Configuration**: Sets system identifier to "sp_mis" for deployment classification -- **Registry Access Control**: Optional JavaScript-based restriction - that makes registrant forms read-only for non-admin users -- **Client-Side Enforcement**: Patches ``FormController`` and - ``ListController`` to hide Create/Edit/Delete buttons and force - readonly mode +- **Registry Access Control**: Optional restriction withholding create, + write and delete on registrant records from non-admin users +- **Server-Side Enforcement**: Applied in the access check every write + passes through, so it holds over RPC and data import as well as in the + web client — the New, Edit and Delete buttons disappear because Odoo + stamps the view from the same access result Key Models ~~~~~~~~~~ @@ -61,8 +62,9 @@ After installing: 1. Navigate to **Settings > SP-MIS Settings** 2. Enable **Restrict Registry Edits to Admin Only** to enforce read-only registry access for non-admin users -3. When enabled, non-admin users see registrant forms in readonly mode - with hidden create/edit/delete buttons +3. When enabled, non-admin users can still read the registry, but + creating, editing and deleting registrants is refused — and the + corresponding buttons are not shown 4. Restriction applies only to ``res.partner`` views; program-related operations remain available based on role @@ -82,15 +84,17 @@ Implementation Details The registry restriction uses: - **Config Parameter**: ``spp_starter.registry_admin_only_crud`` - (default: True) -- **JSON-RPC Endpoint**: ``/spp_starter_sp_mis/registry_restriction`` - checks restriction status -- **JavaScript Patches**: Modifies ``FormController`` and - ``ListController`` for ``res.partner`` model + (default: True), marked ``noupdate`` so an administrator's choice + survives module upgrades +- **Access Check**: ``res.partner._check_access`` withholds create, + write and unlink on records flagged ``is_registrant`` +- **Promotion Guard**: ``write`` refuses setting ``is_registrant`` on a + plain contact, which would otherwise add a registrant in two allowed + steps - **Admin Check**: Users in ``spp_security.group_spp_admin`` bypass all restrictions -- **MutationObserver**: Monitors DOM changes to re-apply restrictions - dynamically +- **Scope**: Only registrant records are affected, so the Contacts app + stays usable Included Modules ~~~~~~~~~~~~~~~~ @@ -119,6 +123,17 @@ Dependencies Changelog ========= +19.0.2.1.0 +~~~~~~~~~~ + +- fix(starter_sp_mis): make the registry restriction hold and stop it + re-locking itself. Enforcement moves from a JavaScript patch Odoo 19 + no longer reads to the access check every create, write and delete + passes through, so it applies over RPC and data import too; promoting + a plain contact into the registry is refused as well. The setting is + marked ``noupdate``, with a migration for databases where an upgrade + would otherwise keep switching it back on (#1142) + 19.0.2.0.0 ~~~~~~~~~~ diff --git a/spp_starter_sp_mis/__init__.py b/spp_starter_sp_mis/__init__.py index 8042e9421..d33610325 100644 --- a/spp_starter_sp_mis/__init__.py +++ b/spp_starter_sp_mis/__init__.py @@ -1,3 +1,2 @@ # Part of OpenSPP. See LICENSE file for full copyright and licensing details. -from . import controllers from . import models diff --git a/spp_starter_sp_mis/__manifest__.py b/spp_starter_sp_mis/__manifest__.py index 254a020a8..3bc4ee1b7 100644 --- a/spp_starter_sp_mis/__manifest__.py +++ b/spp_starter_sp_mis/__manifest__.py @@ -4,7 +4,7 @@ "name": "OpenSPP Starter: SP-MIS", "summary": "Complete SP-MIS bundle with Social Registry, Programs, and Service Points", "category": "OpenSPP", - "version": "19.0.2.0.0", + "version": "19.0.2.1.0", "sequence": 1, "author": "OpenSPP.org", "website": "https://github.com/OpenSPP/OpenSPP2", @@ -24,11 +24,6 @@ "data/config_parameters.xml", "views/res_config_settings_views.xml", ], - "assets": { - "web.assets_backend": [ - "spp_starter_sp_mis/static/src/js/registry_restriction.js", - ], - }, "demo": [], "images": [], "application": False, diff --git a/spp_starter_sp_mis/controllers/__init__.py b/spp_starter_sp_mis/controllers/__init__.py deleted file mode 100644 index 972165815..000000000 --- a/spp_starter_sp_mis/controllers/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Part of OpenSPP. See LICENSE file for full copyright and licensing details. -from . import main diff --git a/spp_starter_sp_mis/controllers/main.py b/spp_starter_sp_mis/controllers/main.py deleted file mode 100644 index ca73594fb..000000000 --- a/spp_starter_sp_mis/controllers/main.py +++ /dev/null @@ -1,13 +0,0 @@ -# Part of OpenSPP. See LICENSE file for full copyright and licensing details. - -from odoo import http -from odoo.http import request - - -class SPMISController(http.Controller): - @http.route("/spp_starter_sp_mis/registry_restriction", type="jsonrpc", auth="user") - def get_registry_restriction(self): - """Return whether registry CRUD is restricted to admin only.""" - # nosemgrep: odoo-sudo-without-context - value = request.env["ir.config_parameter"].sudo().get_param("spp_starter.registry_admin_only_crud", "False") - return {"restricted": value == "True"} diff --git a/spp_starter_sp_mis/data/config_parameters.xml b/spp_starter_sp_mis/data/config_parameters.xml index 346b54d85..04a4fee7a 100644 --- a/spp_starter_sp_mis/data/config_parameters.xml +++ b/spp_starter_sp_mis/data/config_parameters.xml @@ -9,9 +9,17 @@ sp_mis - - - spp_starter.registry_admin_only_crud - True - + + + + spp_starter.registry_admin_only_crud + True + + diff --git a/spp_starter_sp_mis/migrations/19.0.2.1.0/post-migration.py b/spp_starter_sp_mis/migrations/19.0.2.1.0/post-migration.py new file mode 100644 index 000000000..4b5437af8 --- /dev/null +++ b/spp_starter_sp_mis/migrations/19.0.2.1.0/post-migration.py @@ -0,0 +1,35 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +"""Stop re-locking the registry on every upgrade (OP#1142). + +``config_registry_admin_only_crud`` is now declared ``noupdate="1"`` so an +administrator's choice survives module upgrades. That declaration only governs +xml_ids created from now on: ``_build_update_xmlids_query`` upserts with +``ON CONFLICT … DO UPDATE SET (model, res_id, write_date)`` and never touches +the stored ``noupdate`` flag of an existing row. So every database that already +carries this xml_id keeps ``noupdate = false``, and each upgrade re-applies +``value = True`` — silently re-locking a registry that was deliberately opened, +which is the bug this module set out to fix. + +The flag has to be flipped in the row itself, once. +""" + +import logging + +_logger = logging.getLogger(__name__) + + +def migrate(cr, version): + if not version: + return + + cr.execute( + """ + UPDATE ir_model_data + SET noupdate = true + WHERE module = 'spp_starter_sp_mis' + AND name = 'config_registry_admin_only_crud' + AND noupdate IS NOT TRUE + """ + ) + if cr.rowcount: + _logger.info("Marked config_registry_admin_only_crud noupdate; the setting now survives upgrades") diff --git a/spp_starter_sp_mis/models/__init__.py b/spp_starter_sp_mis/models/__init__.py index cdb421fac..db6e0091d 100644 --- a/spp_starter_sp_mis/models/__init__.py +++ b/spp_starter_sp_mis/models/__init__.py @@ -1,2 +1,3 @@ # Part of OpenSPP. See LICENSE file for full copyright and licensing details. from . import res_config_settings +from . import res_partner diff --git a/spp_starter_sp_mis/models/res_config_settings.py b/spp_starter_sp_mis/models/res_config_settings.py index 86a6520d8..86937f489 100644 --- a/spp_starter_sp_mis/models/res_config_settings.py +++ b/spp_starter_sp_mis/models/res_config_settings.py @@ -2,6 +2,10 @@ from odoo import fields, models +# Storage key for the registry access-control setting. Imported by res_partner +# so the toggle and its enforcement can never drift apart. +REGISTRY_ADMIN_ONLY_CRUD_PARAM = "spp_starter.registry_admin_only_crud" + class ResConfigSettings(models.TransientModel): _inherit = "res.config.settings" @@ -12,6 +16,30 @@ class ResConfigSettings(models.TransientModel): "Only administrators can add, modify, or remove registrants. " "Other users can still view all registry data but cannot make changes." ), - default=True, - config_parameter="spp_starter.registry_admin_only_crud", + config_parameter=REGISTRY_ADMIN_ONLY_CRUD_PARAM, ) + + def set_values(self): + """Persist the toggle explicitly, including when it is off (OP#1142). + + Odoo stores a False ``config_parameter`` by *deleting* the row, and + ``default_get`` falls back to the field's ``default`` when the row is + missing. Pairing that with ``default=True`` made "off" unrepresentable: + the toggle sprang back on at every reload, while enforcement — which + reads a missing row as False — quietly went unrestricted, so the form + claimed the registry was locked when it was open. + + Writing the value as a string keeps "off" a stored fact rather than an + absence, which is what makes the two sides agree. The install default + now comes from ``data/config_parameters.xml`` instead of a field + default, so a missing row can no longer mean "on". + """ + super().set_values() + # Writing a system configuration parameter. ir.config_parameter is + # restricted to Settings managers, and the settings form is already + # gated on that group, so this widens nothing. + # nosemgrep: odoo-sudo-without-context + self.env["ir.config_parameter"].sudo().set_param( + REGISTRY_ADMIN_ONLY_CRUD_PARAM, + "True" if self.is_registry_admin_only_crud else "False", + ) diff --git a/spp_starter_sp_mis/models/res_partner.py b/spp_starter_sp_mis/models/res_partner.py new file mode 100644 index 000000000..b4f24c688 --- /dev/null +++ b/spp_starter_sp_mis/models/res_partner.py @@ -0,0 +1,100 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. + +from odoo import _, models +from odoo.exceptions import AccessError + +from .res_config_settings import REGISTRY_ADMIN_ONLY_CRUD_PARAM + +ADMIN_GROUP = "spp_security.group_spp_admin" + +# Operations the setting withholds from non-admins. ``read`` is deliberately +# absent: the whole point is that everyone keeps visibility of the registry. +RESTRICTED_OPERATIONS = ("create", "write", "unlink") + + +class ResPartner(models.Model): + _inherit = "res.partner" + + def _is_registry_crud_restricted(self): + """Whether registry changes are currently withheld from this user.""" + if self.env.user.has_group(ADMIN_GROUP): + return False + # Reads one system setting in order to decide whether to *withhold* + # access. Every user has to be able to read it, and it exposes nothing + # beyond the flag itself. + # nosemgrep: odoo-sudo-without-context + param = self.env["ir.config_parameter"].sudo().get_param(REGISTRY_ADMIN_ONLY_CRUD_PARAM, "False") + return param == "True" + + def _make_registry_access_error(self): + return AccessError( + _( + "Registry records are restricted to administrators. Ask an administrator to make " + "this change, or turn off 'Restrict Registry Edits to Admin Only' in SP-MIS Settings." + ) + ) + + def write(self, vals): + """Refuse promoting a plain contact into the registry (OP#1142 review). + + ``_check_access('write')`` filters on the record's *current* values, and + there is no post-write pass, so a restricted user could create a plain + contact and then flip ``is_registrant`` on it — two allowed steps adding + up to a registrant they were never allowed to create. The same move + promotes any existing contact. Unflagging needs no guard: the record is + already a registrant when the check runs. + """ + if vals.get("is_registrant") and self._is_registry_crud_restricted(): + # Reading the current flag to find what is being promoted; filtering + # as the user would recurse back into the access check. + # nosemgrep: odoo-sudo-without-context + promoted = self.sudo().filtered(lambda partner: not partner.is_registrant) + if promoted: + raise self._make_registry_access_error() + return super().write(vals) + + def _check_access(self, operation): + """Withhold registrant create/write/unlink from non-admins (OP#1142). + + This is the single chokepoint behind ``check_access``, ``has_access`` + and the ORM's own create/write/unlink guards, so one override both + refuses the change — over RPC and import as much as through the UI — + and takes New/Edit/Delete off registry views for free, because + ``ir.ui.view._postprocess_access_rights`` stamps ``create="false"`` onto + an arch whenever ``has_access('create')`` comes back False. That is what + actually removes the button; the previous JavaScript patch assigned a + ``canCreate`` property Odoo 19's ListController never reads. + + Enforcement is scoped to registrants so the setting cannot lock the + whole Contacts app. On a populated recordset that is a plain filter, and + it is the check the ORM applies to real writes — including the + post-create pass — so it holds regardless of how the call arrives. + + The empty recordset is the model-level probe, used by views and by + ``create()`` before any record exists. There is nothing to filter there, + so registrant intent is read from the context both registry actions + carry. Note that ``get_view`` documents its result as depending only on + access rights and a few context keys, so should that ever stop flowing, + the button reappears but the refusal above still stands — the failure + mode is cosmetic, not a loss of enforcement. + """ + result = super()._check_access(operation) + if result is not None or operation not in RESTRICTED_OPERATIONS: + return result + if not self._is_registry_crud_restricted(): + return None + + if self: + # Reads is_registrant to decide what to refuse. Filtering as the + # user would recurse straight back into this check, and nothing is + # returned but ids the caller already holds. + # nosemgrep: odoo-sudo-without-context + forbidden = self.browse(self.sudo().filtered("is_registrant").ids) + if not forbidden: + return None + elif self.env.context.get("default_is_registrant"): + forbidden = self + else: + return None + + return forbidden, self._make_registry_access_error diff --git a/spp_starter_sp_mis/readme/DESCRIPTION.md b/spp_starter_sp_mis/readme/DESCRIPTION.md index 60fba3cb2..a0ab4c171 100644 --- a/spp_starter_sp_mis/readme/DESCRIPTION.md +++ b/spp_starter_sp_mis/readme/DESCRIPTION.md @@ -1,11 +1,11 @@ -Starter bundle for Social Protection Management Information System (SP-MIS) deployments. Extends `spp_starter_social_registry` with program management, approval workflows, and service delivery capabilities. Adds optional client-side registry access control to restrict registrant editing to administrators. +Starter bundle for Social Protection Management Information System (SP-MIS) deployments. Extends `spp_starter_social_registry` with program management, approval workflows, and service delivery capabilities. Adds optional registry access control, enforced on the server, to restrict registrant editing to administrators. ### Key Capabilities - **Bundle Management**: Installs social registry foundation plus program management modules in a single deployment - **Starter Type Configuration**: Sets system identifier to "sp_mis" for deployment classification -- **Registry Access Control**: Optional JavaScript-based restriction that makes registrant forms read-only for non-admin users -- **Client-Side Enforcement**: Patches `FormController` and `ListController` to hide Create/Edit/Delete buttons and force readonly mode +- **Registry Access Control**: Optional restriction withholding create, write and delete on registrant records from non-admin users +- **Server-Side Enforcement**: Applied in the access check every write passes through, so it holds over RPC and data import as well as in the web client — the New, Edit and Delete buttons disappear because Odoo stamps the view from the same access result ### Key Models @@ -21,7 +21,7 @@ After installing: 1. Navigate to **Settings > SP-MIS Settings** 2. Enable **Restrict Registry Edits to Admin Only** to enforce read-only registry access for non-admin users -3. When enabled, non-admin users see registrant forms in readonly mode with hidden create/edit/delete buttons +3. When enabled, non-admin users can still read the registry, but creating, editing and deleting registrants is refused — and the corresponding buttons are not shown 4. Restriction applies only to `res.partner` views; program-related operations remain available based on role ### UI Location @@ -34,11 +34,11 @@ After installing: The registry restriction uses: -- **Config Parameter**: `spp_starter.registry_admin_only_crud` (default: True) -- **JSON-RPC Endpoint**: `/spp_starter_sp_mis/registry_restriction` checks restriction status -- **JavaScript Patches**: Modifies `FormController` and `ListController` for `res.partner` model +- **Config Parameter**: `spp_starter.registry_admin_only_crud` (default: True), marked `noupdate` so an administrator's choice survives module upgrades +- **Access Check**: `res.partner._check_access` withholds create, write and unlink on records flagged `is_registrant` +- **Promotion Guard**: `write` refuses setting `is_registrant` on a plain contact, which would otherwise add a registrant in two allowed steps - **Admin Check**: Users in `spp_security.group_spp_admin` bypass all restrictions -- **MutationObserver**: Monitors DOM changes to re-apply restrictions dynamically +- **Scope**: Only registrant records are affected, so the Contacts app stays usable ### Included Modules diff --git a/spp_starter_sp_mis/readme/HISTORY.md b/spp_starter_sp_mis/readme/HISTORY.md index 4aaf9afef..41131d098 100644 --- a/spp_starter_sp_mis/readme/HISTORY.md +++ b/spp_starter_sp_mis/readme/HISTORY.md @@ -1,3 +1,7 @@ +### 19.0.2.1.0 + +- fix(starter_sp_mis): make the registry restriction hold and stop it re-locking itself. Enforcement moves from a JavaScript patch Odoo 19 no longer reads to the access check every create, write and delete passes through, so it applies over RPC and data import too; promoting a plain contact into the registry is refused as well. The setting is marked `noupdate`, with a migration for databases where an upgrade would otherwise keep switching it back on (#1142) + ### 19.0.2.0.0 - Initial migration to OpenSPP2 diff --git a/spp_starter_sp_mis/static/description/index.html b/spp_starter_sp_mis/static/description/index.html index 623eada22..df0a16983 100644 --- a/spp_starter_sp_mis/static/description/index.html +++ b/spp_starter_sp_mis/static/description/index.html @@ -373,8 +373,8 @@

OpenSPP Starter: SP-MIS

Starter bundle for Social Protection Management Information System (SP-MIS) deployments. Extends spp_starter_social_registry with program management, approval workflows, and service delivery -capabilities. Adds optional client-side registry access control to -restrict registrant editing to administrators.

+capabilities. Adds optional registry access control, enforced on the +server, to restrict registrant editing to administrators.

Key Capabilities

@@ -417,8 +418,9 @@

Configuration

  • Navigate to Settings > SP-MIS Settings
  • Enable Restrict Registry Edits to Admin Only to enforce read-only registry access for non-admin users
  • -
  • When enabled, non-admin users see registrant forms in readonly mode -with hidden create/edit/delete buttons
  • +
  • When enabled, non-admin users can still read the registry, but +creating, editing and deleting registrants is refused — and the +corresponding buttons are not shown
  • Restriction applies only to res.partner views; program-related operations remain available based on role
  • @@ -439,15 +441,17 @@

    Implementation Details

    The registry restriction uses:

    @@ -478,6 +482,18 @@

    Changelog

    +

    19.0.2.1.0

    + +
    +

    19.0.2.0.0