diff --git a/docker-compose.yml b/docker-compose.yml
index 9ef28f0d1..9d293b23e 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -59,7 +59,7 @@ services:
POSTGRES_DB: openspp
volumes:
- postgres_data:/var/lib/postgresql
- - ./docker/postgresql.conf:/etc/postgresql/postgresql.conf:ro
+ - ./docker/postgresql.conf:/etc/postgresql/postgresql.conf:ro,z
healthcheck:
test: ["CMD-SHELL", "pg_isready -U odoo -d postgres"]
interval: 5s
diff --git a/spp_consent/README.rst b/spp_consent/README.rst
index a025e6cd0..ac3e8493b 100644
--- a/spp_consent/README.rst
+++ b/spp_consent/README.rst
@@ -193,6 +193,18 @@ Dependencies
Changelog
=========
+19.0.2.0.2
+~~~~~~~~~~
+
+- fix(security): the Tier-3 ``group_registry_read`` group can read the
+ consent models the registrant form depends on. This module extends
+ that form, and its models were granted only to the Tier-2
+ ``group_registry_viewer`` group, so a Tier-3-scoped role hit an
+ AccessError opening a registrant. The two wizard models are
+ deliberately not included: their entry points are restricted to the
+ officer and manager tiers, so read access would grant nothing usable
+ while exposing other users' in-progress wizard rows.
+
19.0.2.0.1
~~~~~~~~~~
diff --git a/spp_consent/__manifest__.py b/spp_consent/__manifest__.py
index 5c9570502..1fb338285 100644
--- a/spp_consent/__manifest__.py
+++ b/spp_consent/__manifest__.py
@@ -3,7 +3,7 @@
{
"name": "OpenSPP Consent",
"category": "OpenSPP",
- "version": "19.0.2.0.1",
+ "version": "19.0.2.0.2",
"summary": """DPV-aligned consent management for social protection programs.
Implements ISO/IEC TS 27560:2023 consent record information structure
diff --git a/spp_consent/readme/HISTORY.md b/spp_consent/readme/HISTORY.md
index c9a9bc34c..425bb25aa 100644
--- a/spp_consent/readme/HISTORY.md
+++ b/spp_consent/readme/HISTORY.md
@@ -1,3 +1,7 @@
+### 19.0.2.0.2
+
+- fix(security): the Tier-3 `group_registry_read` group can read the consent models the registrant form depends on. This module extends that form, and its models were granted only to the Tier-2 `group_registry_viewer` group, so a Tier-3-scoped role hit an AccessError opening a registrant. The two wizard models are deliberately not included: their entry points are restricted to the officer and manager tiers, so read access would grant nothing usable while exposing other users' in-progress wizard rows.
+
### 19.0.2.0.1
- fix(views): apply `spp_registry.x2many_no_padding` widget to the Consents list on registrant forms, and hide the table entirely when there are no consents (showing a muted info line instead) — matches the empty-state treatment of read-only / no-create lists elsewhere (#943).
diff --git a/spp_consent/security/ir.model.access.csv b/spp_consent/security/ir.model.access.csv
index d9ad4ad11..8ea4fcaae 100644
--- a/spp_consent/security/ir.model.access.csv
+++ b/spp_consent/security/ir.model.access.csv
@@ -35,3 +35,10 @@ access_spp_bulk_record_consent_wizard_viewer,Bulk Record Consent Wizard Viewer A
access_spp_bulk_record_consent_wizard_officer,Bulk Record Consent Wizard Officer Access,spp_consent.model_spp_bulk_record_consent_wizard,spp_registry.group_registry_officer,1,1,1,0
access_spp_bulk_record_consent_wizard_manager,Bulk Record Consent Wizard Manager Access,spp_consent.model_spp_bulk_record_consent_wizard,spp_registry.group_registry_manager,1,1,1,1
access_spp_bulk_record_consent_wizard_admin,Bulk Record Consent Wizard Admin Access,spp_consent.model_spp_bulk_record_consent_wizard,spp_security.group_spp_admin,1,1,1,1
+access_spp_consent_registry_read,Consent Registry Read Access,spp_consent.model_spp_consent,spp_registry.group_registry_read,1,0,0,0
+access_spp_consent_purpose_registry_read,Consent Purpose Registry Read Access,spp_consent.model_spp_consent_purpose,spp_registry.group_registry_read,1,0,0,0
+access_spp_consent_personal_data_registry_read,Consent Personal Data Registry Read Access,spp_consent.model_spp_consent_personal_data,spp_registry.group_registry_read,1,0,0,0
+access_spp_consent_processing_registry_read,Consent Processing Registry Read Access,spp_consent.model_spp_consent_processing,spp_registry.group_registry_read,1,0,0,0
+access_spp_consent_notice_registry_read,Consent Notice Registry Read Access,spp_consent.model_spp_consent_notice,spp_registry.group_registry_read,1,0,0,0
+access_spp_consent_history_registry_read,Consent History Registry Read Access,spp_consent.model_spp_consent_history,spp_registry.group_registry_read,1,0,0,0
+access_spp_consent_org_type_registry_read,Consent Org Type Registry Read Access,spp_consent.model_spp_consent_org_type,spp_registry.group_registry_read,1,0,0,0
diff --git a/spp_consent/static/description/index.html b/spp_consent/static/description/index.html
index 556808ef9..6ee4b98fb 100644
--- a/spp_consent/static/description/index.html
+++ b/spp_consent/static/description/index.html
@@ -589,6 +589,19 @@
+
19.0.2.0.2
+
+- fix(security): the Tier-3 group_registry_read group can read the
+consent models the registrant form depends on. This module extends
+that form, and its models were granted only to the Tier-2
+group_registry_viewer group, so a Tier-3-scoped role hit an
+AccessError opening a registrant. The two wizard models are
+deliberately not included: their entry points are restricted to the
+officer and manager tiers, so read access would grant nothing usable
+while exposing other users’ in-progress wizard rows.
+
+
+
19.0.2.0.1
- fix(views): apply spp_registry.x2many_no_padding widget to the
@@ -598,7 +611,7 @@
19.0.2.0.1
(#943).
-
+
19.0.2.0.0
- Initial migration to OpenSPP2
diff --git a/spp_consent/tests/__init__.py b/spp_consent/tests/__init__.py
index c3b086edc..1d8414783 100644
--- a/spp_consent/tests/__init__.py
+++ b/spp_consent/tests/__init__.py
@@ -10,4 +10,5 @@
from . import test_privacy_notices
from . import test_record_consent_wizard
from . import test_registrant
+from . import test_registry_read_access
from . import test_security
diff --git a/spp_consent/tests/test_registry_read_access.py b/spp_consent/tests/test_registry_read_access.py
new file mode 100644
index 000000000..ca246d97a
--- /dev/null
+++ b/spp_consent/tests/test_registry_read_access.py
@@ -0,0 +1,44 @@
+# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
+"""Tier-3 ``group_registry_read`` must cover this module's registrant-form models.
+
+``spp_registry.group_registry_viewer`` (Tier-2) implies ``group_registry_read``
+(Tier-3), so any ACL granted only to the viewer tier disappears for a role
+scoped to the read tier. ``spp_consent/views/registrant_view.xml`` extends the registrant form,
+so a read-tier role that opens a registrant hits an AccessError unless the
+Tier-3 group carries these models too.
+"""
+
+from odoo import Command
+from odoo.tests import TransactionCase, tagged
+
+_MODELS = [
+ "spp.consent",
+ "spp.consent.purpose",
+ "spp.consent.personal.data",
+ "spp.consent.processing",
+ "spp.consent.notice",
+ "spp.consent.history",
+ "spp.consent.org.type",
+]
+
+
+@tagged("post_install", "-at_install")
+class TestRegistryReadAccess(TransactionCase):
+ def test_tier3_registry_read_covers_registrant_form_models(self):
+ user = self.env["res.users"].create(
+ {
+ "name": "Tier3 Reader",
+ "login": "tier3_reader_spp_consent",
+ "email": "tier3_reader_spp_consent@example.com",
+ "group_ids": [
+ Command.link(self.env.ref("base.group_user").id),
+ Command.link(self.env.ref("spp_registry.group_registry_read").id),
+ ],
+ }
+ )
+ Access = self.env["ir.model.access"].with_user(user)
+ missing = sorted(m for m in _MODELS if not Access.check(m, "read", raise_exception=False))
+ self.assertFalse(
+ missing,
+ "Tier-3 group_registry_read cannot read registrant-form models: " + ", ".join(missing),
+ )
diff --git a/spp_irrigation/README.rst b/spp_irrigation/README.rst
index 06303904c..b7179bdc1 100644
--- a/spp_irrigation/README.rst
+++ b/spp_irrigation/README.rst
@@ -100,6 +100,14 @@ Dependencies
Changelog
=========
+19.0.2.1.1
+~~~~~~~~~~
+
+- fix(security): the Tier-3 ``group_registry_read`` group can read
+ ``spp.irrigation.asset``. The registrant form renders
+ ``irrigation_asset_ids``, and the model was granted only to the Tier-2
+ ``group_registry_viewer`` group.
+
19.0.2.1.0
~~~~~~~~~~
diff --git a/spp_irrigation/__manifest__.py b/spp_irrigation/__manifest__.py
index 99b64208c..8547febe4 100644
--- a/spp_irrigation/__manifest__.py
+++ b/spp_irrigation/__manifest__.py
@@ -5,7 +5,7 @@
{
"name": "OpenSPP Irrigation",
"category": "OpenSPP",
- "version": "19.0.2.1.0",
+ "version": "19.0.2.1.1",
"sequence": 1,
"author": "OpenSPP.org",
"website": "https://github.com/OpenSPP/OpenSPP2",
diff --git a/spp_irrigation/readme/HISTORY.md b/spp_irrigation/readme/HISTORY.md
index 8a595c3ed..ebe962bb6 100644
--- a/spp_irrigation/readme/HISTORY.md
+++ b/spp_irrigation/readme/HISTORY.md
@@ -1,3 +1,7 @@
+### 19.0.2.1.1
+
+- fix(security): the Tier-3 `group_registry_read` group can read `spp.irrigation.asset`. The registrant form renders `irrigation_asset_ids`, and the model was granted only to the Tier-2 `group_registry_viewer` group.
+
### 19.0.2.1.0
- feat(views): add an "Irrigation" tab on the farm (group) form so per-farm irrigation assets are reachable without leaving the farm record; backed by a new `irrigation_asset_ids` One2many on `res.partner` (inverse of the existing `farm_id`)
diff --git a/spp_irrigation/security/ir.model.access.csv b/spp_irrigation/security/ir.model.access.csv
index 27bb5ed85..31b4bce2b 100644
--- a/spp_irrigation/security/ir.model.access.csv
+++ b/spp_irrigation/security/ir.model.access.csv
@@ -1,4 +1,5 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_spp_irrigation_asset,SPP Irrigation Asset Access,model_spp_irrigation_asset,spp_irrigation.group_irrigation_manager,1,1,1,1
-access_spp_irrigation_asset_registry_read,SPP Irrigation Asset Read,model_spp_irrigation_asset,spp_registry.group_registry_viewer,1,0,0,0
+access_spp_irrigation_asset_registry_viewer,SPP Irrigation Asset Viewer Read,model_spp_irrigation_asset,spp_registry.group_registry_viewer,1,0,0,0
+access_spp_irrigation_asset_registry_read,SPP Irrigation Asset Registry Read,model_spp_irrigation_asset,spp_registry.group_registry_read,1,0,0,0
diff --git a/spp_irrigation/static/description/index.html b/spp_irrigation/static/description/index.html
index 355f9974f..d80c18fb9 100644
--- a/spp_irrigation/static/description/index.html
+++ b/spp_irrigation/static/description/index.html
@@ -468,6 +468,15 @@
+
19.0.2.1.1
+
+- fix(security): the Tier-3 group_registry_read group can read
+spp.irrigation.asset. The registrant form renders
+irrigation_asset_ids, and the model was granted only to the Tier-2
+group_registry_viewer group.
+
+
+
19.0.2.1.0
- feat(views): add an “Irrigation” tab on the farm (group) form so
@@ -476,7 +485,7 @@
19.0.2.1.0
res.partner (inverse of the existing farm_id)
-
+
19.0.2.0.0
- Initial migration to OpenSPP2
diff --git a/spp_irrigation/tests/__init__.py b/spp_irrigation/tests/__init__.py
index 0986f84bd..d9c16d65b 100644
--- a/spp_irrigation/tests/__init__.py
+++ b/spp_irrigation/tests/__init__.py
@@ -1,3 +1,4 @@
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
from . import test_irrigation_asset
from . import test_irrigation_edge_cases
+from . import test_registry_read_access
diff --git a/spp_irrigation/tests/test_registry_read_access.py b/spp_irrigation/tests/test_registry_read_access.py
new file mode 100644
index 000000000..22c39774a
--- /dev/null
+++ b/spp_irrigation/tests/test_registry_read_access.py
@@ -0,0 +1,39 @@
+# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
+"""Tier-3 ``group_registry_read`` must cover this module's registrant-form models.
+
+``spp_registry.group_registry_viewer`` (Tier-2) implies ``group_registry_read``
+(Tier-3), so any ACL granted only to the viewer tier disappears for a role
+scoped to the read tier. ``spp_irrigation/views/irrigation_view.xml`` renders
+``irrigation_asset_ids`` on the registrant form,
+so a read-tier role that opens a registrant hits an AccessError unless the
+Tier-3 group carries these models too.
+"""
+
+from odoo import Command
+from odoo.tests import TransactionCase, tagged
+
+_MODELS = [
+ "spp.irrigation.asset",
+]
+
+
+@tagged("post_install", "-at_install")
+class TestRegistryReadAccess(TransactionCase):
+ def test_tier3_registry_read_covers_registrant_form_models(self):
+ user = self.env["res.users"].create(
+ {
+ "name": "Tier3 Reader",
+ "login": "tier3_reader_spp_irrigation",
+ "email": "tier3_reader_spp_irrigation@example.com",
+ "group_ids": [
+ Command.link(self.env.ref("base.group_user").id),
+ Command.link(self.env.ref("spp_registry.group_registry_read").id),
+ ],
+ }
+ )
+ Access = self.env["ir.model.access"].with_user(user)
+ missing = sorted(m for m in _MODELS if not Access.check(m, "read", raise_exception=False))
+ self.assertFalse(
+ missing,
+ "Tier-3 group_registry_read cannot read registrant-form models: " + ", ".join(missing),
+ )
diff --git a/spp_programs/README.rst b/spp_programs/README.rst
index 03263225f..9436f7903 100644
--- a/spp_programs/README.rst
+++ b/spp_programs/README.rst
@@ -254,6 +254,16 @@ Dependencies
Changelog
=========
+19.0.2.3.4
+~~~~~~~~~~
+
+- fix(security): the Tier-3 ``group_registry_read`` group can read
+ ``spp.cycle`` and ``spp.cycle.membership``. Both were granted only to
+ the Tier-2 ``group_registry_viewer`` group, yet the registrant form
+ depends on both: the entitlement lists render ``cycle_id``, and the
+ membership list renders ``latest_cycle_state``, which is computed by
+ searching ``spp.cycle.membership`` as the acting user.
+
19.0.2.3.3
~~~~~~~~~~
diff --git a/spp_programs/__manifest__.py b/spp_programs/__manifest__.py
index 4e4cda8b8..cdb136049 100644
--- a/spp_programs/__manifest__.py
+++ b/spp_programs/__manifest__.py
@@ -4,7 +4,7 @@
"name": "OpenSPP Programs",
"summary": "Manage programs, cycles, beneficiary enrollment, entitlements (cash and in-kind), payments, and fund tracking for social protection.",
"category": "OpenSPP/Core",
- "version": "19.0.2.3.3",
+ "version": "19.0.2.3.4",
"sequence": 1,
"author": "OpenSPP.org",
"website": "https://github.com/OpenSPP/OpenSPP2",
diff --git a/spp_programs/readme/HISTORY.md b/spp_programs/readme/HISTORY.md
index ddc705e4c..02a0af95c 100644
--- a/spp_programs/readme/HISTORY.md
+++ b/spp_programs/readme/HISTORY.md
@@ -1,3 +1,7 @@
+### 19.0.2.3.4
+
+- fix(security): the Tier-3 `group_registry_read` group can read `spp.cycle` and `spp.cycle.membership`. Both were granted only to the Tier-2 `group_registry_viewer` group, yet the registrant form depends on both: the entitlement lists render `cycle_id`, and the membership list renders `latest_cycle_state`, which is computed by searching `spp.cycle.membership` as the acting user.
+
### 19.0.2.3.3
- fix(security): the operation lock is now guarded on create as well as write. Restricting only writes to `is_locked` / `locked_reason` left creation unguarded, so a program officer could create a cycle or program already locked and bypass the check entirely — and then could not clear the lock again, since clearing it goes through the guarded write, leaving a self-inflicted lockout only a system administrator could undo. The check is shared by both paths; `sudo()` and system administrators are unaffected, so the async pipeline keeps managing the lock as before.
diff --git a/spp_programs/security/ir.model.access.csv b/spp_programs/security/ir.model.access.csv
index c8c8ed5b3..cb71a2eb8 100644
--- a/spp_programs/security/ir.model.access.csv
+++ b/spp_programs/security/ir.model.access.csv
@@ -406,3 +406,5 @@ access_spp_program_membership_exit_wizard_manager,Program Membership Exit Wizard
access_spp_program_membership_exit_wizard_admin,Program Membership Exit Wizard Admin Access,spp_programs.model_spp_program_membership_exit_wizard,spp_security.group_spp_admin,1,1,1,1
access_spp_deduplication_setup_wizard_manager,Deduplication Setup Wizard Manager Access,spp_programs.model_spp_deduplication_setup_wizard,group_programs_manager,1,1,1,1
access_spp_deduplication_setup_wizard_admin,Deduplication Setup Wizard Admin Access,spp_programs.model_spp_deduplication_setup_wizard,spp_security.group_spp_admin,1,1,1,1
+access_spp_cycle_registry_read,Cycle Registry Read,spp_programs.model_spp_cycle,spp_registry.group_registry_read,1,0,0,0
+access_spp_cycle_membership_registry_read,Cycle Membership Registry Read,spp_programs.model_spp_cycle_membership,spp_registry.group_registry_read,1,0,0,0
diff --git a/spp_programs/static/description/index.html b/spp_programs/static/description/index.html
index 231476114..ac614eea7 100644
--- a/spp_programs/static/description/index.html
+++ b/spp_programs/static/description/index.html
@@ -658,6 +658,17 @@
+
19.0.2.3.4
+
+- fix(security): the Tier-3 group_registry_read group can read
+spp.cycle and spp.cycle.membership. Both were granted only to
+the Tier-2 group_registry_viewer group, yet the registrant form
+depends on both: the entitlement lists render cycle_id, and the
+membership list renders latest_cycle_state, which is computed by
+searching spp.cycle.membership as the acting user.
+
+
+
19.0.2.3.3
- fix(security): the operation lock is now guarded on create as well as
@@ -671,7 +682,7 @@
19.0.2.3.3
the lock as before.
-
+
19.0.2.3.2
- fix(security): the Program Viewer role no longer carries the Tier-2
@@ -685,7 +696,7 @@
19.0.2.3.2
already-assigned users on upgrade.
-
+
19.0.2.3.1
- fix(security): make the async operation lock a server-side boundary.
@@ -702,7 +713,7 @@
19.0.2.3.1
acquire/release from the initiating user keeps working.
-
+
19.0.2.3.0
- feat(spp_programs): Duplicate Detection is a card with an Add
@@ -727,7 +738,7 @@
19.0.2.3.0
still blocked its own re-adding (#1171)
-
+
19.0.2.2.1
- fix(spp_programs): stop Enroll Eligible undoing a deliberate pause. A
@@ -737,7 +748,7 @@
19.0.2.2.1
Pausing is a decision that only Resume reverses (#1117)
-
+
19.0.2.1.3
- fix(security): align Program Viewer / Validator / Cycle Approver roles
@@ -756,7 +767,7 @@
19.0.2.1.3
cross-references — only the dedicated top-level menu disappears.
-
+
19.0.2.1.2
- fix(security): add global ir.rule records on
@@ -770,7 +781,7 @@
19.0.2.1.2
no-op for users with no center areas (global roles).
-
+
19.0.2.1.1
- fix(views): apply spp_registry.x2many_no_padding widget to the
@@ -779,7 +790,7 @@
19.0.2.1.1
19 inserts on inline list-in-form views (#943).
-
+
19.0.2.0.11
- Fix TypeError: 'NoneType' object is not iterable when clicking
@@ -790,7 +801,7 @@
19.0.2.0.11
omit the state filter instead of crashing on tuple(None)
-
+
19.0.2.0.10
- Increase parallel-safe channel limits (cycle, eligibility_manager,
@@ -803,7 +814,7 @@
19.0.2.0.10
submission on double-click
-
+
19.0.2.0.9
- Add context flags (skip_registrant_statistics,
@@ -816,7 +827,7 @@
19.0.2.0.9
_compute_has_members
-
+
19.0.2.0.8
- Replace OFFSET pagination with NTILE-based ID-range batching in all
@@ -827,7 +838,7 @@
19.0.2.0.8
program and cycle
-
+
19.0.2.0.7
- Bulk membership creation using raw SQL INSERT ON CONFLICT DO NOTHING
@@ -836,7 +847,7 @@
19.0.2.0.7
_add_beneficiaries with bulk SQL path
-
+
19.0.2.0.6
- Remove unused entitlement_base_model.py (dead code, never imported)
@@ -845,34 +856,34 @@ 19.0.2.0.6
payment, and fund tests (172 → 492 tests)
-
+
19.0.2.0.5
- Batch create entitlements and payments instead of one-by-one ORM
creates
-
+
19.0.2.0.4
- Fetch fund balance once per approval batch instead of per entitlement
-
+
19.0.2.0.3
- Replace cycle computed fields (total_amount, entitlements_count,
approval flags) with SQL aggregation queries
-
+
19.0.2.0.2
- Add composite indexes for frequent query patterns on entitlements and
program memberships
-
+
19.0.2.0.1
- Replace Python-level uniqueness checks with SQL UNIQUE constraints for
@@ -881,7 +892,7 @@
19.0.2.0.1
constraint creation
-
+
19.0.2.0.0
- Initial migration to OpenSPP2
diff --git a/spp_programs/tests/__init__.py b/spp_programs/tests/__init__.py
index 86ea5bfae..9c0b90101 100644
--- a/spp_programs/tests/__init__.py
+++ b/spp_programs/tests/__init__.py
@@ -46,3 +46,4 @@
from . import test_approve_entitlements_program_isolation
from . import test_payment_batch_payment_ids
from . import test_program_viewer_registry_scope
+from . import test_registry_read_access
diff --git a/spp_programs/tests/test_registry_read_access.py b/spp_programs/tests/test_registry_read_access.py
new file mode 100644
index 000000000..78a829b6b
--- /dev/null
+++ b/spp_programs/tests/test_registry_read_access.py
@@ -0,0 +1,40 @@
+# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
+"""Tier-3 ``group_registry_read`` must cover this module's registrant-form models.
+
+``spp_registry.group_registry_viewer`` (Tier-2) implies ``group_registry_read``
+(Tier-3), so any ACL granted only to the viewer tier disappears for a role
+scoped to the read tier. ``spp_programs/views/registrant_view.xml`` renders ``cycle_id`` in the
+entitlement lists on the registrant form,
+so a read-tier role that opens a registrant hits an AccessError unless the
+Tier-3 group carries these models too.
+"""
+
+from odoo import Command
+from odoo.tests import TransactionCase, tagged
+
+_MODELS = [
+ "spp.cycle",
+ "spp.cycle.membership",
+]
+
+
+@tagged("post_install", "-at_install")
+class TestRegistryReadAccess(TransactionCase):
+ def test_tier3_registry_read_covers_registrant_form_models(self):
+ user = self.env["res.users"].create(
+ {
+ "name": "Tier3 Reader",
+ "login": "tier3_reader_spp_programs",
+ "email": "tier3_reader_spp_programs@example.com",
+ "group_ids": [
+ Command.link(self.env.ref("base.group_user").id),
+ Command.link(self.env.ref("spp_registry.group_registry_read").id),
+ ],
+ }
+ )
+ Access = self.env["ir.model.access"].with_user(user)
+ missing = sorted(m for m in _MODELS if not Access.check(m, "read", raise_exception=False))
+ self.assertFalse(
+ missing,
+ "Tier-3 group_registry_read cannot read registrant-form models: " + ", ".join(missing),
+ )