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.
43 lines
1.3 KiB
Python
43 lines
1.3 KiB
Python
import pytest
|
|
from sqlalchemy import inspect
|
|
from sqlalchemy.ext.asyncio import AsyncEngine
|
|
|
|
pytestmark = [
|
|
pytest.mark.integration,
|
|
pytest.mark.postgres,
|
|
pytest.mark.asyncio(loop_scope="session"),
|
|
]
|
|
|
|
EXPECTED_TABLES = {
|
|
"tenants",
|
|
"tenant_domains",
|
|
"api_keys",
|
|
"source_files",
|
|
"ingestion_jobs",
|
|
"ingestion_job_events",
|
|
"alembic_version",
|
|
}
|
|
|
|
|
|
async def test_migrations_create_schema_from_empty_database(postgres_engine: AsyncEngine) -> None:
|
|
async with postgres_engine.connect() as connection:
|
|
table_names = await connection.run_sync(
|
|
lambda sync_conn: inspect(sync_conn).get_table_names()
|
|
)
|
|
|
|
assert EXPECTED_TABLES.issubset(set(table_names))
|
|
|
|
|
|
async def test_tenant_domains_enforces_one_row_per_tenant_and_key(
|
|
postgres_engine: AsyncEngine,
|
|
) -> None:
|
|
"""The unique constraint is what stops the same domain being registered
|
|
twice for a tenant while still letting two tenants share a key.
|
|
"""
|
|
async with postgres_engine.connect() as connection:
|
|
constraints = await connection.run_sync(
|
|
lambda sync_conn: inspect(sync_conn).get_unique_constraints("tenant_domains")
|
|
)
|
|
|
|
assert any(constraint["column_names"] == ["tenant_id", "domain"] for constraint in constraints)
|