Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions spp_mis_demo_v2/tests/test_access_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
47 changes: 31 additions & 16 deletions spp_starter_sp_mis/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
~~~~~~~~~~~~~~~~
Expand All @@ -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
~~~~~~~~~~
Expand All @@ -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

Expand All @@ -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
~~~~~~~~~~~~~~~~
Expand Down Expand Up @@ -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
~~~~~~~~~~

Expand Down
1 change: 0 additions & 1 deletion spp_starter_sp_mis/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
from . import controllers
from . import models
7 changes: 1 addition & 6 deletions spp_starter_sp_mis/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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,
Expand Down
2 changes: 0 additions & 2 deletions spp_starter_sp_mis/controllers/__init__.py

This file was deleted.

13 changes: 0 additions & 13 deletions spp_starter_sp_mis/controllers/main.py

This file was deleted.

18 changes: 13 additions & 5 deletions spp_starter_sp_mis/data/config_parameters.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,17 @@
<field name="value">sp_mis</field>
</record>

<!-- Restrict Registry CRUD to Admin only by default -->
<record id="config_registry_admin_only_crud" model="ir.config_parameter">
<field name="key">spp_starter.registry_admin_only_crud</field>
<field name="value">True</field>
</record>
<!--
Restrict Registry CRUD to Admin only by default.

noupdate: this is the *install* default, not a managed value. Without it
every module upgrade would re-apply True and silently re-lock a registry
the administrator had deliberately opened (OP#1142).
-->
<data noupdate="1">
<record id="config_registry_admin_only_crud" model="ir.config_parameter">
<field name="key">spp_starter.registry_admin_only_crud</field>
<field name="value">True</field>
</record>
</data>
</odoo>
35 changes: 35 additions & 0 deletions spp_starter_sp_mis/migrations/19.0.2.1.0/post-migration.py
Original file line number Diff line number Diff line change
@@ -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")
1 change: 1 addition & 0 deletions spp_starter_sp_mis/models/__init__.py
Original file line number Diff line number Diff line change
@@ -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
32 changes: 30 additions & 2 deletions spp_starter_sp_mis/models/res_config_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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(
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
REGISTRY_ADMIN_ONLY_CRUD_PARAM,
"True" if self.is_registry_admin_only_crud else "False",
)
100 changes: 100 additions & 0 deletions spp_starter_sp_mis/models/res_partner.py
Original file line number Diff line number Diff line change
@@ -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")
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
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)
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
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
16 changes: 8 additions & 8 deletions spp_starter_sp_mis/readme/DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
Expand All @@ -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

Expand Down
4 changes: 4 additions & 0 deletions spp_starter_sp_mis/readme/HISTORY.md
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading