refactor(#356): DSAR-Fachlogik vollstaendig aus dem Core extrahiert — dsar_collect/dsar_erase in die 5 beteiligten Contracts (contacts, mail, tasks, calendar, kommunikation); core/jobs.py sammelt/loescht nur Core-eigene Daten und iteriert generisch ueber die Plugin-Registry; neue Plugins liefern DSAR-Kategorien ohne Core-Aenderung; Counts-/Category-Keys unveraendert; tasks/contracts.py von Patch-Artefakt bereinigt
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
fixes #356
This commit is contained in:
@@ -2,6 +2,11 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.plugins.builtins.calendar.models import Calendar, CalendarEntry, CalendarEntryLink
|
||||
from app.plugins.builtins.contracts import get_contract_registry
|
||||
|
||||
@@ -15,6 +20,33 @@ class CalendarContract:
|
||||
CalendarEntry = CalendarEntry
|
||||
CalendarEntryLink = CalendarEntryLink
|
||||
|
||||
@staticmethod
|
||||
async def dsar_collect(
|
||||
db: AsyncSession, tenant_id: Any, user_id: Any
|
||||
) -> dict[str, Any]:
|
||||
"""GDPR Art. 15: collect calendar entries owned by the user."""
|
||||
cal_entries = (
|
||||
await db.execute(
|
||||
select(CalendarEntry).where(
|
||||
CalendarEntry.tenant_id == tenant_id,
|
||||
CalendarEntry.owner_id == user_id,
|
||||
CalendarEntry.deleted_at.is_(None),
|
||||
).limit(1000)
|
||||
)
|
||||
).scalars().all()
|
||||
return {
|
||||
"calendar_entries": [
|
||||
{
|
||||
"id": str(e.id),
|
||||
"title": e.title,
|
||||
"entry_type": e.entry_type,
|
||||
"start_at": e.start_at.isoformat() if e.start_at else None,
|
||||
"end_at": e.end_at.isoformat() if e.end_at else None,
|
||||
}
|
||||
for e in cal_entries
|
||||
]
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_function(cls, name: str):
|
||||
"""Return a callable exposed by this contract, or None if absent."""
|
||||
|
||||
@@ -50,6 +50,62 @@ class ContactsContract:
|
||||
"total": results[0],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def dsar_collect(
|
||||
db: AsyncSession, tenant_id: Any, user_id: Any
|
||||
) -> dict[str, Any]:
|
||||
"""GDPR Art. 15: collect contact data owned by the user.
|
||||
|
||||
Owned by the contacts plugin — core/jobs.py calls this generically
|
||||
via the contract, it must not know contact internals.
|
||||
"""
|
||||
contacts = (
|
||||
await db.execute(
|
||||
select(Contact).where(
|
||||
Contact.tenant_id == tenant_id,
|
||||
Contact.owner_id == user_id,
|
||||
Contact.deleted_at.is_(None),
|
||||
)
|
||||
)
|
||||
).scalars().all()
|
||||
return {
|
||||
"contacts": [
|
||||
{
|
||||
"id": str(c.id),
|
||||
"type": c.type,
|
||||
"displayname": c.displayname,
|
||||
"email_1": c.email_1,
|
||||
"email_2": c.email_2,
|
||||
}
|
||||
for c in contacts
|
||||
]
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def dsar_erase(
|
||||
db: AsyncSession, tenant_id: Any, user_id: Any
|
||||
) -> dict[str, int]:
|
||||
"""GDPR Art. 17: soft-delete contacts owned by the user.
|
||||
|
||||
Soft-delete via deleted_at (audit history must remain intact — it is
|
||||
a business record, not personal data of the subject; retention
|
||||
policy governs its cleanup).
|
||||
"""
|
||||
from datetime import UTC, datetime
|
||||
|
||||
contacts = (
|
||||
await db.execute(
|
||||
select(Contact).where(
|
||||
Contact.tenant_id == tenant_id,
|
||||
Contact.owner_id == user_id,
|
||||
Contact.deleted_at.is_(None),
|
||||
)
|
||||
)
|
||||
).scalars().all()
|
||||
for c in contacts:
|
||||
c.deleted_at = datetime.now(UTC)
|
||||
return {"contacts_soft_deleted": len(contacts)}
|
||||
|
||||
@classmethod
|
||||
def get_function(cls, name: str):
|
||||
"""Return a callable exposed by this contract, or None if absent."""
|
||||
|
||||
@@ -13,6 +13,11 @@ instead of importing from internal modules directly.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.plugins.builtins.contracts import get_contract_registry
|
||||
from app.plugins.builtins.kommunikation.miniapp_registry import (
|
||||
MiniAppDef,
|
||||
@@ -74,6 +79,31 @@ class KommunikationContract:
|
||||
CommMessage = CommMessage
|
||||
CommParticipant = CommParticipant
|
||||
|
||||
@staticmethod
|
||||
async def dsar_collect(
|
||||
db: AsyncSession, tenant_id: Any, user_id: Any
|
||||
) -> dict[str, Any]:
|
||||
"""GDPR Art. 15: collect communication messages sent by the user."""
|
||||
comm_messages = (
|
||||
await db.execute(
|
||||
select(CommMessage).where(
|
||||
CommMessage.sender_id == user_id,
|
||||
CommMessage.tenant_id == tenant_id,
|
||||
).limit(1000)
|
||||
)
|
||||
).scalars().all()
|
||||
return {
|
||||
"comm_messages": [
|
||||
{
|
||||
"id": str(m.id),
|
||||
"sender_type": m.sender_type,
|
||||
"content": m.content[:500],
|
||||
"created_at": m.created_at.isoformat() if m.created_at else None,
|
||||
}
|
||||
for m in comm_messages
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
# ─── self-registration ───
|
||||
|
||||
|
||||
@@ -16,6 +16,11 @@ instead of importing from internal modules directly.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.plugins.builtins.contracts import get_contract_registry
|
||||
from app.plugins.builtins.mail.models import Mail, MailAccount
|
||||
|
||||
@@ -33,6 +38,32 @@ class MailContract:
|
||||
Mail = Mail
|
||||
MailAccount = MailAccount
|
||||
|
||||
@staticmethod
|
||||
async def dsar_collect(
|
||||
db: AsyncSession, tenant_id: Any, user_id: Any
|
||||
) -> dict[str, Any]:
|
||||
"""GDPR Art. 15: collect mail account data owned by the user."""
|
||||
mail_accounts = (
|
||||
await db.execute(
|
||||
select(MailAccount).where(
|
||||
MailAccount.tenant_id == tenant_id,
|
||||
MailAccount.user_id == user_id,
|
||||
).limit(500)
|
||||
)
|
||||
).scalars().all()
|
||||
return {
|
||||
"mail_accounts": [
|
||||
{
|
||||
"id": str(a.id),
|
||||
"email_address": a.email_address,
|
||||
"display_name": a.display_name,
|
||||
"is_shared": a.is_shared,
|
||||
"is_active": a.is_active,
|
||||
}
|
||||
for a in mail_accounts
|
||||
]
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_function(cls, name: str):
|
||||
"""Return a callable exposed by this contract, or None if absent."""
|
||||
|
||||
@@ -13,6 +13,11 @@ instead of importing from internal modules directly.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import or_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.plugins.builtins.contracts import get_contract_registry
|
||||
from app.plugins.builtins.tasks.models import Task
|
||||
from app.plugins.builtins.tasks.services import (
|
||||
@@ -45,6 +50,33 @@ class TasksContract:
|
||||
# ─── models (read-only for queries) ───
|
||||
Task = Task
|
||||
|
||||
@staticmethod
|
||||
async def dsar_collect(
|
||||
db: AsyncSession, tenant_id: Any, user_id: Any
|
||||
) -> dict[str, Any]:
|
||||
"""GDPR Art. 15: collect tasks owned by or assigned to the user."""
|
||||
tasks = (
|
||||
await db.execute(
|
||||
select(Task).where(
|
||||
or_(Task.owner_id == user_id, Task.assigned_to == user_id),
|
||||
Task.tenant_id == tenant_id,
|
||||
Task.deleted_at.is_(None),
|
||||
).limit(1000)
|
||||
)
|
||||
).scalars().all()
|
||||
return {
|
||||
"tasks": [
|
||||
{
|
||||
"id": str(t.id),
|
||||
"title": t.title,
|
||||
"status": t.status,
|
||||
"priority": t.priority,
|
||||
"due_date": t.due_date.isoformat() if t.due_date else None,
|
||||
}
|
||||
for t in tasks
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
# ─── self-registration ───
|
||||
|
||||
|
||||
Reference in New Issue
Block a user