Preserve tenant module entitlement state
This commit is contained in:
@@ -25,3 +25,8 @@ pattern contract documented in
|
|||||||
[`docs/INTERFACE_PATTERN_MIGRATION.md`](docs/INTERFACE_PATTERN_MIGRATION.md).
|
[`docs/INTERFACE_PATTERN_MIGRATION.md`](docs/INTERFACE_PATTERN_MIGRATION.md).
|
||||||
Manifest-provided documentation topics back contextual help for tenant fields,
|
Manifest-provided documentation topics back contextual help for tenant fields,
|
||||||
governance limits, permission blockers, and lifecycle consequences.
|
governance limits, permission blockers, and lifecycle consequences.
|
||||||
|
|
||||||
|
Core's `module_entitlements` tenant-setting key is reserved. Generic tenant
|
||||||
|
updates preserve it even when replacing the remaining settings document;
|
||||||
|
system and tenant module administrators change it through the Admin module's
|
||||||
|
dedicated, revision-checked module policy APIs.
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from govoplan_core.core.access import (
|
|||||||
TenantContextSwitcher,
|
TenantContextSwitcher,
|
||||||
)
|
)
|
||||||
from govoplan_core.core.change_sequence import ChangeSequenceEntry, decode_sequence_watermark, encode_sequence_watermark, record_change, sequence_watermark_is_expired
|
from govoplan_core.core.change_sequence import ChangeSequenceEntry, decode_sequence_watermark, encode_sequence_watermark, record_change, sequence_watermark_is_expired
|
||||||
|
from govoplan_core.core.module_entitlements import MODULE_ENTITLEMENTS_KEY
|
||||||
from govoplan_core.core.principal_cache import invalidate_auth_principals
|
from govoplan_core.core.principal_cache import invalidate_auth_principals
|
||||||
from govoplan_core.core.runtime import get_registry
|
from govoplan_core.core.runtime import get_registry
|
||||||
from govoplan_core.db.session import get_session
|
from govoplan_core.db.session import get_session
|
||||||
@@ -667,7 +668,14 @@ def _apply_tenant_content_updates(tenant: Tenant, payload: TenantUpdateRequest)
|
|||||||
if payload.default_locale is not None:
|
if payload.default_locale is not None:
|
||||||
tenant.default_locale = _normalized_tenant_locale(payload.default_locale)
|
tenant.default_locale = _normalized_tenant_locale(payload.default_locale)
|
||||||
if payload.settings is not None:
|
if payload.settings is not None:
|
||||||
tenant.settings = payload.settings
|
current_settings = dict(tenant.settings or {})
|
||||||
|
next_settings = dict(payload.settings)
|
||||||
|
next_settings.pop(MODULE_ENTITLEMENTS_KEY, None)
|
||||||
|
if MODULE_ENTITLEMENTS_KEY in current_settings:
|
||||||
|
next_settings[MODULE_ENTITLEMENTS_KEY] = current_settings[
|
||||||
|
MODULE_ENTITLEMENTS_KEY
|
||||||
|
]
|
||||||
|
tenant.settings = next_settings
|
||||||
|
|
||||||
|
|
||||||
def _normalized_optional_text(value: str | None) -> str | None:
|
def _normalized_optional_text(value: str | None) -> str | None:
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ manifest = ModuleManifest(
|
|||||||
id="tenancy.lifecycle-and-settings",
|
id="tenancy.lifecycle-and-settings",
|
||||||
title="Administer tenant lifecycle and settings",
|
title="Administer tenant lifecycle and settings",
|
||||||
summary="Tenancy adds explicit tenant creation, activation, context resolution, and tenant-owned settings over Core's shared scope storage.",
|
summary="Tenancy adds explicit tenant creation, activation, context resolution, and tenant-owned settings over Core's shared scope storage.",
|
||||||
body="A tenant is a concrete administrative and data boundary. Tenant lifecycle changes must preserve ownership and recovery guarantees for module-owned records. Tenancy contributes system tenant management and tenant settings to the shared administration workspace; without this module, the Core and Access baseline can operate in single-scope compatibility mode.",
|
body="A tenant is a concrete administrative and data boundary. Tenant lifecycle changes must preserve ownership and recovery guarantees for module-owned records. Tenancy contributes system tenant management and tenant settings to the shared administration workspace; without this module, the Core and Access baseline can operate in single-scope compatibility mode. Core-reserved module entitlement settings are managed only through the Admin module's tenant-module policy endpoints and are preserved when generic tenant settings are replaced.",
|
||||||
documentation_types=("admin",),
|
documentation_types=("admin",),
|
||||||
audience=("system_admin", "tenant_admin", "operator"),
|
audience=("system_admin", "tenant_admin", "operator"),
|
||||||
related_modules=("access", "admin", "audit"),
|
related_modules=("access", "admin", "audit"),
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from govoplan_tenancy.backend.api.v1.routes import (
|
|||||||
_require_tenant_update_permissions,
|
_require_tenant_update_permissions,
|
||||||
)
|
)
|
||||||
from govoplan_tenancy.backend.api.v1.schemas import TenantUpdateRequest
|
from govoplan_tenancy.backend.api.v1.schemas import TenantUpdateRequest
|
||||||
|
from govoplan_core.core.module_entitlements import MODULE_ENTITLEMENTS_KEY
|
||||||
from govoplan_tenancy.backend.lifecycle import (
|
from govoplan_tenancy.backend.lifecycle import (
|
||||||
TENANT_EVENT_CREATED,
|
TENANT_EVENT_CREATED,
|
||||||
TENANT_EVENT_DELETION_REQUESTED,
|
TENANT_EVENT_DELETION_REQUESTED,
|
||||||
@@ -121,6 +122,26 @@ class TenantUpdateHelperTests(unittest.TestCase):
|
|||||||
self.assertEqual("en", tenant.default_locale)
|
self.assertEqual("en", tenant.default_locale)
|
||||||
self.assertEqual({"theme": "contrast"}, tenant.settings)
|
self.assertEqual({"theme": "contrast"}, tenant.settings)
|
||||||
|
|
||||||
|
def test_tenant_content_update_preserves_reserved_module_entitlements(self) -> None:
|
||||||
|
entitlement = {"schema_version": 1, "revision": 4}
|
||||||
|
tenant = SimpleNamespace(
|
||||||
|
name="Old",
|
||||||
|
description=None,
|
||||||
|
default_locale="en",
|
||||||
|
settings={MODULE_ENTITLEMENTS_KEY: entitlement, "theme": "old"},
|
||||||
|
)
|
||||||
|
payload = TenantUpdateRequest(
|
||||||
|
settings={
|
||||||
|
"theme": "contrast",
|
||||||
|
MODULE_ENTITLEMENTS_KEY: {"revision": 999},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
_apply_tenant_content_updates(tenant, payload) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
self.assertEqual("contrast", tenant.settings["theme"])
|
||||||
|
self.assertEqual(entitlement, tenant.settings[MODULE_ENTITLEMENTS_KEY])
|
||||||
|
|
||||||
def test_tenant_status_update_prevents_suspending_current_tenant(self) -> None:
|
def test_tenant_status_update_prevents_suspending_current_tenant(self) -> None:
|
||||||
tenant = SimpleNamespace(id="tenant-1", is_active=True)
|
tenant = SimpleNamespace(id="tenant-1", is_active=True)
|
||||||
principal = FakePrincipal({"system:tenants:suspend"}, tenant_id="tenant-1")
|
principal = FakePrincipal({"system:tenants:suspend"}, tenant_id="tenant-1")
|
||||||
|
|||||||
Reference in New Issue
Block a user