From 092c2d20fb682e3b9b0150eec9d1be829268f438 Mon Sep 17 00:00:00 2001 From: Agent Zero Date: Thu, 27 Aug 2026 17:38:06 +0200 Subject: [PATCH] =?UTF-8?q?fix(#356):=20DSAR-Sammlung=20auf=20Contract-Zug?= =?UTF-8?q?riff=20umgestellt=20=E2=80=94=204=20Core=E2=86=92Plugin-Imports?= =?UTF-8?q?=20(mail/tasks/calendar/kommunikation)=20nutzen=20jetzt=20get?= =?UTF-8?q?=5Fcontract();=20MailContract=20exponiert=20MailAccount;=20Impo?= =?UTF-8?q?rtError-Fallback-Semantik=20unver=C3=A4ndert;=20Checker=204?= =?UTF-8?q?=E2=86=920=20Verst=C3=B6=C3=9Fe;=20DSAR-Suite=204/4=20passed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixes #356 --- PROGRESS.md | 9 +++++ app/core/jobs.py | 49 ++++++++++++++++---------- app/plugins/builtins/mail/contracts.py | 7 ++-- 3 files changed, 44 insertions(+), 21 deletions(-) diff --git a/PROGRESS.md b/PROGRESS.md index b24862f..7a89a63 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -12,6 +12,15 @@ **Gates:** ruff exit=0 · tsc --noEmit exit=0 · pytest 11 passed · Vitest 2 passed +## Welle 2 — Cross-Plugin/DSAR-Fix (2026-08-27) + +| Finding | Issue | Fix | Verifikation (Live-Messung) | +|---|---|---|---| +| 4 Core→Plugin-Imports in `core/jobs.py` DSAR-Sammlung (mail/tasks/calendar/kommunikation Models direkt importiert statt über Contracts) | [#356](https://forgejo.media-on.de/Leopoldadmin/leocrm/issues/356) | 4 Blöcke auf `get_contract()` umgestellt (bestehende Registry, ARCH-014-Schutz); MailContract exponiert `MailAccount`; ImportError-Fallback-Semantik unverändert | Checker vor Fix: 4 Verstöße → nach Fix: **0 Verstöße** (480 Dateien); DSAR-Suite `test_g1_dsar.py` 4/4 passed (Funktionserhalt); ruff: nur 2 Vorbestand-N811 | +| P16 manuell klassifiziert: `app.models.contact`-Imports in core/jobs.py + worker.py sind KEINE Verstöße (Contact liegt im Core-Models-Layer) | — | Keine Aktion nötig, dokumentiert in #356 | Checker-Regex deckt nur `app.plugins.*` ab — korrekt so | + +**Gates:** ruff modified-files grün (2 Vorbestand N811 ausgenommen) · Cross-Plugin-Checker 0 · pytest DSAR 4/4 + ## Welle 1 — Plugin-Lifecycle-Fix (2026-08-27) | Finding | Issue | Fix | Verifikation (Live-Messung) | diff --git a/app/core/jobs.py b/app/core/jobs.py index 704c438..eda5b31 100644 --- a/app/core/jobs.py +++ b/app/core/jobs.py @@ -172,6 +172,7 @@ async def _dsar_collect_user_data(db: Any, tenant_id: str, user_id: str) -> dict from app.models.contact import Contact from app.models.notification import Notification from app.models.user import User + from app.plugins.builtins.contracts import get_contract uid = PyUUID(user_id) tid = PyUUID(tenant_id) @@ -256,13 +257,16 @@ async def _dsar_collect_user_data(db: Any, tenant_id: str, user_id: str) -> dict # ── Categories promised by the dsgvo-export route docstring ── # (G1-b: mail accounts, tasks, calendar entries, comm messages) try: - from app.plugins.builtins.mail.models import MailAccount + mail_contract = get_contract("mail") + if mail_contract is None: + raise ImportError("mail plugin not active") + mail_account_m = mail_contract.MailAccount mail_accounts = ( await db.execute( - sa_select(MailAccount).where( - MailAccount.tenant_id == tid, - MailAccount.user_id == uid, + sa_select(mail_account_m).where( + mail_account_m.tenant_id == tid, + mail_account_m.user_id == uid, ).limit(500) ) ).scalars().all() @@ -280,14 +284,17 @@ async def _dsar_collect_user_data(db: Any, tenant_id: str, user_id: str) -> dict pass try: - from app.plugins.builtins.tasks.models import Task as TaskModel + tasks_contract = get_contract("tasks") + if tasks_contract is None: + raise ImportError("tasks plugin not active") + task_m = tasks_contract.Task tasks = ( await db.execute( - sa_select(TaskModel).where( - sa_or_(TaskModel.owner_id == uid, TaskModel.assigned_to == uid), - TaskModel.tenant_id == tid, - TaskModel.deleted_at.is_(None), + sa_select(task_m).where( + sa_or_(task_m.owner_id == uid, task_m.assigned_to == uid), + task_m.tenant_id == tid, + task_m.deleted_at.is_(None), ).limit(1000) ) ).scalars().all() @@ -305,14 +312,17 @@ async def _dsar_collect_user_data(db: Any, tenant_id: str, user_id: str) -> dict pass try: - from app.plugins.builtins.calendar.models import CalendarEntry as CalEntry + cal_contract = get_contract("calendar") + if cal_contract is None: + raise ImportError("calendar plugin not active") + cal_entry_m = cal_contract.CalendarEntry cal_entries = ( await db.execute( - sa_select(CalEntry).where( - CalEntry.tenant_id == tid, - CalEntry.owner_id == uid, - CalEntry.deleted_at.is_(None), + sa_select(cal_entry_m).where( + cal_entry_m.tenant_id == tid, + cal_entry_m.owner_id == uid, + cal_entry_m.deleted_at.is_(None), ).limit(1000) ) ).scalars().all() @@ -330,13 +340,16 @@ async def _dsar_collect_user_data(db: Any, tenant_id: str, user_id: str) -> dict pass try: - from app.plugins.builtins.kommunikation.models import CommMessage + komm_contract = get_contract("kommunikation") + if komm_contract is None: + raise ImportError("kommunikation plugin not active") + comm_msg_m = komm_contract.CommMessage comm_messages = ( await db.execute( - sa_select(CommMessage).where( - CommMessage.sender_id == uid, - CommMessage.tenant_id == tid, + sa_select(comm_msg_m).where( + comm_msg_m.sender_id == uid, + comm_msg_m.tenant_id == tid, ).limit(1000) ) ).scalars().all() diff --git a/app/plugins/builtins/mail/contracts.py b/app/plugins/builtins/mail/contracts.py index 55f2862..18053b4 100644 --- a/app/plugins/builtins/mail/contracts.py +++ b/app/plugins/builtins/mail/contracts.py @@ -17,20 +17,21 @@ instead of importing from internal modules directly. from __future__ import annotations from app.plugins.builtins.contracts import get_contract_registry -from app.plugins.builtins.mail.models import Mail +from app.plugins.builtins.mail.models import Mail, MailAccount class MailContract: """Public API surface for the mail plugin. - Exposes the ``Mail`` ORM model so that other plugins can query the - mails table without importing from ``mail.models`` directly. + Exposes ORM models so that other plugins can query the mail tables + without importing from ``mail.models`` directly. """ contract_name = "mail" # ─── models ─── Mail = Mail + MailAccount = MailAccount @classmethod def get_function(cls, name: str):