fix(arch): externes Audit — 13 Backend-Fixes (Workspace-Modules, Tenant-Manifeste, Lifecycle, Contracts, Permissions)
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
Verifikation: Alle 17 Audit-Findings gegen den Code geprueft — alle bestaetigt. Backend-Lifecycle-Fixes umgesetzt; 4 Frontend-Plugin-Architektur-Punkte als Phase Q in die Roadmap eingeplant. - P1 list_workspaces: Module + User-Counts gebuendelt laden (Editor-Overwrite-Bug) - P1 active-manifests: Tenant-Deaktivierung (tenant_plugin_activation) filtern - P1 uninstall: volle Service-Deactivation VOR registry.uninstall() - P1 ContractRegistry: DB-Aktivstatus-Guard (Restart-Edge-Case) + Re-Activate - P1/P2 Field-Definitions: voller Lifecycle (register/unregister) im Service - P1/P2 Contact-Felddefinitionen (39) ins ContactsPlugin-Manifest verschoben - P1 12 fehlende Permission-Keys registriert (AST-Scan: 0 fehlend) - P2 contact_folder -> ContactsPlugin; ENTITY_PLUGIN_OWNERS wird befuellt - P2 Entity-Permission-Fallback fail-closed statt contacts:read - P2 forgejo_error_reporter is_core=False; DMS is_core=True (ADR-020) - P2 Worker: Contacts-Trash-Cleanup ins Plugin (get_job_modules-Discovery) - P1/P2 DSGVO-Export delegiert an DSAR-Collector (kein Core->Contacts) - P2 False-green Tests korrigiert (or True, veraltete Route-Count-Assertion) Verifikation: tests/test_audit_architecture_fixes.py 17/17; Regressionen gruen (contacts_lifecycle, entity_registry, workspace_scopes, rbac, lifecycle_service); Combo-Order-Test 35/35; Cross-Plugin-Checker 497/0; compileall sauber; ruff auf 7-Error-Baseline. Doku: PROGRESS.md Audit-Section, PLATFORM_ROADMAP.md Phase Q (Q1-Q4), plugin-development-guide.md Lifecycle, permissions.md Katalog.
This commit is contained in:
@@ -60,6 +60,9 @@ class ContractRegistry:
|
||||
cls._instance._contracts: dict[str, Any] = {}
|
||||
cls._instance._loaded: set[str] = set()
|
||||
cls._instance._unregistered: set[str] = set()
|
||||
# Plugins whose DB record says active=False (audit restart edge
|
||||
# case) — marked once at API startup, see main.py lifespan.
|
||||
cls._instance._db_inactive: set[str] = set()
|
||||
return cls._instance
|
||||
|
||||
# ─── registration ───
|
||||
@@ -92,20 +95,51 @@ class ContractRegistry:
|
||||
|
||||
On first access the registry attempts to lazy-load the plugin's
|
||||
``contracts`` module, which will register itself on import.
|
||||
"""
|
||||
if plugin_name in self._contracts:
|
||||
return self._contracts[plugin_name]
|
||||
|
||||
Audit P1 (contract lazy loading): the DB activation state is checked
|
||||
BEFORE serving or lazy-loading. A plugin that was already inactive
|
||||
when the process started never lands in ``_unregistered`` (it was
|
||||
never deactivated at runtime), so the old guard alone let the lazy
|
||||
loader import its contracts module and resurrect the contract.
|
||||
The permission registry mirrors ``PluginModel.active`` at startup,
|
||||
so an inactive plugin fails closed here. When the permission
|
||||
registry is NOT initialized (worker process, early bootstrap)
|
||||
the legacy lazy-load behaviour is kept.
|
||||
"""
|
||||
# Explicitly unregistered (deactivated): never resurrect via
|
||||
# lazy-loading (ARCH-014) — the deactivated contract must stay gone.
|
||||
if plugin_name in self._unregistered:
|
||||
return None
|
||||
|
||||
# DB activation guard (audit restart edge case): plugins whose DB
|
||||
# record was already inactive when the process started never land in
|
||||
# _unregistered (they were never deactivated at runtime), so lazy
|
||||
# loading could resurrect their contracts. main.py marks them once
|
||||
# at startup; activation clears the marker again.
|
||||
if plugin_name in self._db_inactive:
|
||||
return None
|
||||
|
||||
if plugin_name in self._contracts:
|
||||
return self._contracts[plugin_name]
|
||||
|
||||
if plugin_name not in self._loaded:
|
||||
self._try_lazy_load(plugin_name)
|
||||
|
||||
return self._contracts.get(plugin_name)
|
||||
|
||||
def mark_db_inactive(self, plugin_names: set[str]) -> None:
|
||||
"""Mark plugins as DB-inactive (startup, audit restart edge case).
|
||||
|
||||
Called once from main.py lifespan with the names of plugins whose DB
|
||||
record has active=False. get_contract() fails closed for these.
|
||||
"""
|
||||
self._db_inactive.update(plugin_names)
|
||||
|
||||
def mark_plugin_active(self, plugin_name: str) -> None:
|
||||
"""Clear inactive markers (plugin activated/reinstalled at runtime)."""
|
||||
self._db_inactive.discard(plugin_name)
|
||||
self._unregistered.discard(plugin_name)
|
||||
|
||||
def require_contract(self, plugin_name: str) -> Any:
|
||||
"""Like :meth:`get_contract` but raise if unavailable."""
|
||||
contract = self.get_contract(plugin_name)
|
||||
@@ -148,6 +182,7 @@ class ContractRegistry:
|
||||
"""Clear all state — for unit tests only."""
|
||||
self._contracts.clear()
|
||||
self._loaded.clear()
|
||||
self._db_inactive.clear()
|
||||
|
||||
|
||||
# ─── module-level helpers ───
|
||||
|
||||
Reference in New Issue
Block a user