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:
17
CLAUDE.md
17
CLAUDE.md
@@ -22,10 +22,14 @@ and re-benchmark rather than tune them in place (ADR-0005). Also working: the
|
||||
`chunks` collection bootstrap (`src/infrastructure/qdrant/collection.py`, run as
|
||||
a deployment step via `uv run python -m src.cli.qdrant_bootstrap` — never at
|
||||
startup) and tenant-scoped point upserts (`src/application/points/` behind the
|
||||
`PointStorage` port), so an upload is searchable by the time `201` returns. Not
|
||||
built yet: `/v1/points` CRUD and keyword search (plan 002), `tenant_domains`
|
||||
validation of the `domain` field, and `src/agent/`. That maps to plan 001
|
||||
Phases 1-5 done.
|
||||
`PointStorage` port), so an upload is searchable by the time `201` returns.
|
||||
Also working: `tenant_domains` plus `/v1/domains` (`src/application/domains/`),
|
||||
a strict per-tenant allowlist — `POST /v1/files` rejects an unregistered or
|
||||
disabled `domain` with `400` before anything is written, and domain management
|
||||
sits behind its own `domains:read`/`domains:write` scopes, never `files:write`.
|
||||
Not built yet: `/v1/points` CRUD and keyword search (plan 002), and
|
||||
`src/agent/`. That maps to plan 001 Phases 1-5 done, Phase 6 (runbook, e2e
|
||||
tests, Compose smoke test) not started.
|
||||
|
||||
Architecture decisions live in `docs/adr/` (18 ADRs plus the 0000 template;
|
||||
0001–0004 are `Accepted` — 0004 amended by 0018; 0014 is `Superseded by 0017`;
|
||||
@@ -229,6 +233,11 @@ content_sha256)` idempotency, no terminal job returning to `running`.
|
||||
|
||||
### Postgres conventions (ADR-0009)
|
||||
|
||||
`domain` is never free-form: it must match an `active` `tenant_domains` row for
|
||||
the authenticated tenant (ADR-0009). Domain sets are per-tenant and vary in
|
||||
size. The key itself is immutable — it is denormalized into every Qdrant point
|
||||
payload and into `source_files`, so renaming it is a migration, not an edit.
|
||||
|
||||
UUID primary keys (app-generated), `timestamptz` for all timestamps,
|
||||
`Numeric(18, 8)` for money (never floats), `JSONB` for flexible metadata but
|
||||
typed/indexed columns for query-critical fields, string status columns with
|
||||
|
||||
Reference in New Issue
Block a user