feat(tenant): add tenant_domains allowlist and /v1/domains management API
Why: - Domain values are denormalized into every Qdrant point payload. Without validation, an unregistered or typo'd domain (e.g. "fier" for "fire") silently creates a new partition that retrieval never queries — the file ends up invisible rather than rejected. Tenants also need independently sized domain sets (one may run 14 insurance lines, another 6), which rules out an enum. Changes: - tenant_domains table (migration 41335d162de8) + repository, unique on (tenant_id, domain). - src/application/domains/: ensure_domain_allowed() is the strict-allowlist check now run inside upload_source_file()'s first transaction, before any MinIO object, job row, or Qdrant point is written. - /v1/domains (list/create/patch/disable/enable) gated on its own domains:read/domains:write scopes, deliberately separate from files:write so an upload key cannot create partitions. domain itself is immutable (denormalized into every point payload); only display_name is editable. Disable blocks new uploads without touching already-indexed points. Impact: - BREAKING: POST /v1/files now rejects any domain without an active tenant_domains row (400, unknown_domain). A domain must be created via POST /v1/domains before the first upload to it.
This commit is contained in:
@@ -30,6 +30,7 @@ from anyio import CapacityLimiter, Semaphore, fail_after, to_thread
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||
|
||||
from src.application.auth.context import AuthContext
|
||||
from src.application.domains import ensure_domain_allowed
|
||||
from src.application.files.errors import InvalidUploadError
|
||||
from src.application.files.models import UploadResult
|
||||
from src.application.files.storage_keys import source_file_object_key
|
||||
@@ -118,6 +119,12 @@ async def upload_source_file(
|
||||
|
||||
async with acquire_ingestion_slot(concurrency_limiter):
|
||||
async with sessionmaker() as session:
|
||||
# Strict allowlist, checked inside txn A before anything is written
|
||||
# (ADR-0009). An unregistered domain would otherwise create a new
|
||||
# Qdrant partition silently, leaving the file invisible to
|
||||
# retrieval rather than failing.
|
||||
await ensure_domain_allowed(session, tenant_id=auth.tenant_id, domain=domain)
|
||||
|
||||
existing = await source_files_repo.find_active_by_content_hash(
|
||||
session,
|
||||
tenant_id=auth.tenant_id,
|
||||
|
||||
Reference in New Issue
Block a user