feat(N3): Backend respektiert X-Workspace-ID bei Listen — contacts/dms/mail/calendar (#367)
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
- Core-Resolver resolve_workspace_scope(): Zuweisungs-Check, leere Werte fallen weg; Exemptions System-Admin + workspaces:configure_modules (Editor-Deadlock) - require_workspace_scope(module_key) FastAPI-Dependency (deps.py) - expand_folder_scope(): Ordner-Subtree (zyklensicher) für ContactFolder + DMS Folder; scope_uuid_set() fail-closed - contacts: folder_ids-Subtree + contact_types auf GET /contacts, List-Cache bei aktivem Scope deaktiviert (Cache-Leak-Gefahr) - dms: folder_ids-Subtree + file_types (semantische Matcher) auf /files, Baum-Reduktion auf /folders - mail: account_ids auf /mails, /threads, /accounts - calendar: calendar_ids auf /calendar/entries, /calendars - Frontend-Defaults: getModuleConfig() im workspaceStore, ContactsList default_saved_view_id, Calendar default_view - Tests: 21/21 neu (TDD rot→grün), Regression 81 passed, Checker 0, tsc clean, Vitest grün, Build OK
This commit is contained in:
@@ -146,6 +146,7 @@ async def list_contacts(
|
||||
user_id: uuid.UUID | None = None,
|
||||
is_system_admin: bool = False,
|
||||
cursor: str | None = None,
|
||||
workspace_scope: dict | None = None,
|
||||
) -> dict:
|
||||
"""List contacts with pagination, FTS search, type/folder filter, sorting.
|
||||
|
||||
@@ -155,11 +156,17 @@ async def list_contacts(
|
||||
filtered to ``id > cursor`` instead of using OFFSET. This is much faster
|
||||
for large datasets. When ``cursor`` is not provided, classic page/page_size
|
||||
offset pagination is used (backward compatible).
|
||||
|
||||
Phase N3: ``workspace_scope`` (from X-Workspace-ID) applies folder-subtree
|
||||
and contact-type restrictions as a pure AND on top of all other filters —
|
||||
never a grant. An active scope also disables the list cache (the cache key
|
||||
is workspace-dependent).
|
||||
"""
|
||||
from app.core.visibility import apply_visibility_filter
|
||||
|
||||
# I.4 Performance: Cache simple list queries (no search, no cursor, first 3 pages)
|
||||
use_cache = not search and not cursor and page <= 3 and not folder_id
|
||||
# N3: an active workspace scope is user-dependent — never serve the shared cache
|
||||
use_cache = not search and not cursor and page <= 3 and not folder_id and not workspace_scope
|
||||
cache_key = f"contacts:list:{tenant_id}:{page}:{page_size}:{contact_type or 'all'}:{sort_by}:{sort_order}:{user_id or 'admin'}:{is_system_admin}"
|
||||
if use_cache:
|
||||
from app.core.cache import cache_get
|
||||
@@ -185,6 +192,25 @@ async def list_contacts(
|
||||
if folder_id:
|
||||
base = base.where(Contact.folder_id == uuid.UUID(folder_id))
|
||||
|
||||
# Phase N3: workspace scope (X-Workspace-ID) — pure AND-restriction.
|
||||
# Empty dimension values were already dropped by resolve_workspace_scope.
|
||||
if workspace_scope:
|
||||
from app.models.contact_folder import ContactFolder
|
||||
from app.services.workspace_scope_service import expand_folder_scope
|
||||
|
||||
scope_folder_ids = workspace_scope.get("folder_ids")
|
||||
if isinstance(scope_folder_ids, list) and scope_folder_ids:
|
||||
subtree = await expand_folder_scope(db, ContactFolder, scope_folder_ids)
|
||||
if subtree:
|
||||
base = base.where(Contact.folder_id.in_(subtree))
|
||||
else:
|
||||
# Restrict to a non-existent set: everything is excluded
|
||||
base = base.where(Contact.folder_id.in_(set()))
|
||||
|
||||
scope_types = workspace_scope.get("contact_types")
|
||||
if isinstance(scope_types, list) and scope_types:
|
||||
base = base.where(Contact.type.in_(scope_types))
|
||||
|
||||
if search:
|
||||
base = base.where(
|
||||
Contact.search_tsv.op("@@")(func.plainto_tsquery("german", search))
|
||||
|
||||
Reference in New Issue
Block a user