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:
@@ -12,6 +12,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from src.application.auth.keys import generate_api_key, hash_secret
|
||||
from src.infrastructure.postgres.models.api_key import ApiKey
|
||||
from src.infrastructure.postgres.models.tenant import Tenant
|
||||
from src.infrastructure.postgres.models.tenant_domain import TenantDomain
|
||||
|
||||
|
||||
async def create_tenant(
|
||||
@@ -47,3 +48,27 @@ async def create_api_key(
|
||||
session.add(api_key)
|
||||
await session.flush()
|
||||
return api_key, full_key
|
||||
|
||||
|
||||
async def create_tenant_domain(
|
||||
session: AsyncSession,
|
||||
*,
|
||||
tenant: Tenant,
|
||||
domain: str = "general",
|
||||
status: str = "active",
|
||||
) -> TenantDomain:
|
||||
"""Register a domain so an upload to it passes the allowlist check.
|
||||
|
||||
Uploads reject an unregistered domain (ADR-0009), so any test that uploads
|
||||
needs one of these.
|
||||
"""
|
||||
tenant_domain = TenantDomain(
|
||||
id=uuid.uuid4(),
|
||||
tenant_id=tenant.id,
|
||||
domain=domain,
|
||||
display_name=domain,
|
||||
status=status,
|
||||
)
|
||||
session.add(tenant_domain)
|
||||
await session.flush()
|
||||
return tenant_domain
|
||||
|
||||
Reference in New Issue
Block a user