From 5251990444583b0243dec293678f87aef25601cf Mon Sep 17 00:00:00 2001 From: Ali Zarinkolah Date: Sat, 22 Aug 2026 15:08:39 +0330 Subject: [PATCH] feat(tenant): grant points scopes on provisioned keys Why: - points:read and points:write are named in ADR-0009 but were absent from DEFAULT_SCOPES, so a provisioned key could not reach the read paths that follow. The runbook documented only files:write and domains:write, which understated what a default key can now do. Changes: - Add points:read and points:write to DEFAULT_SCOPES. - Document the full scope table in the runbook, calling out that points:read grants the text of every chunk of every file -- so an upload-only key gets files:write alone. Impact: - Keys issued before this change keep their existing scopes; provisioning does not backfill. Reissue or widen an existing key explicitly. Co-Authored-By: Claude Opus 5 --- docs/runbook.md | 20 ++++++++++++++++---- src/application/tenants/provisioning.py | 8 +++++++- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/docs/runbook.md b/docs/runbook.md index dd97cf7..1399433 100644 --- a/docs/runbook.md +++ b/docs/runbook.md @@ -80,10 +80,22 @@ Re-running with the same `--slug` reuses the tenant and any domains it already has, and issues an **additional** key. Both keys stay valid; this adds a key, it does not rotate one. -Scopes are the security boundary between uploading and managing the allowlist. -Give an upload client `files:write` only. `domains:write` lets its holder create -new domains, which is exactly what the allowlist exists to prevent an upload key -from doing. +Scopes are the security boundary between uploading, reading chunks, and +managing the allowlist. Give an upload client `files:write` only. `domains:write` +lets its holder create new domains, which is exactly what the allowlist exists to +prevent an upload key from doing, and `points:read` lets its holder read the text +of every chunk of every file — so an upload-only key gets neither. + +| Scope | Grants | +|---|---| +| `files:write` | Upload a document and read its ingestion status. | +| `points:read` | Read, list, count, and keyword-search this tenant's points, including `GET /v1/files/{file_id}/points`. | +| `points:write` | Create, edit, reorder, and soft-delete points (plan 002 Phases 3-5; no route uses it yet). | +| `domains:read` / `domains:write` | Inspect and manage the domain allowlist. | +| `admin` | Satisfies every scope check. | + +The command's `--scopes` default issues all of the above except `admin`, which +suits a first operator key; narrow it explicitly for per-client keys. ### Domains after the first one diff --git a/src/application/tenants/provisioning.py b/src/application/tenants/provisioning.py index 1562136..83749aa 100644 --- a/src/application/tenants/provisioning.py +++ b/src/application/tenants/provisioning.py @@ -28,7 +28,13 @@ from src.infrastructure.postgres.repositories import tenants as tenants_repo logger = structlog.get_logger(__name__) -DEFAULT_SCOPES = ("files:write", "domains:read", "domains:write") +DEFAULT_SCOPES = ( + "files:write", + "domains:read", + "domains:write", + "points:read", + "points:write", +) @dataclass(frozen=True)