Why: - nothing over HTTP could create the first tenant: every /v1 route needs an API key, and a key cannot exist before its tenant. The service was unusable by a human without hand-written SQL. Changes: - add `provision_tenant`, owning tenant reuse-or-create, key generation and hashing, and domain registration in one transaction - expose it as `python -m src.cli.provision_tenant`, alongside `alembic upgrade head` and `qdrant_bootstrap` - add `tenants.get_by_slug`/`create` and `api_keys.create` - log `tenant.provisioned` / `api_key.provisioned` with the key prefix only Impact: - a third deployment step; the plaintext key is printed once and never logged or stored (ADR-0011, ADR-0009) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
46 lines
1.3 KiB
Python
46 lines
1.3 KiB
Python
"""API-key lookups and issuance (ADR-0008, ADR-0009).
|
|
|
|
Plain functions over an `AsyncSession` the caller owns. No function here
|
|
commits, rolls back, or closes the session (ADR-0012). Secret comparison
|
|
happens in `src/application/auth`, not here — this module only fetches rows
|
|
by their non-secret `key_prefix`.
|
|
"""
|
|
|
|
import uuid
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from src.infrastructure.postgres.models.api_key import ApiKey
|
|
|
|
|
|
async def get_by_prefix(session: AsyncSession, key_prefix: str) -> ApiKey | None:
|
|
result = await session.execute(select(ApiKey).where(ApiKey.key_prefix == key_prefix))
|
|
return result.scalar_one_or_none()
|
|
|
|
|
|
def create(
|
|
session: AsyncSession,
|
|
*,
|
|
tenant_id: uuid.UUID,
|
|
name: str,
|
|
key_prefix: str,
|
|
key_hash: str,
|
|
scopes: list[str],
|
|
actor_type: str = "backend",
|
|
created_by: str | None = None,
|
|
) -> ApiKey:
|
|
"""Persist an issued key. The caller hashes the secret; this never sees it."""
|
|
api_key = ApiKey(
|
|
id=uuid.uuid4(),
|
|
tenant_id=tenant_id,
|
|
name=name,
|
|
key_prefix=key_prefix,
|
|
key_hash=key_hash,
|
|
scopes=scopes,
|
|
actor_type=actor_type,
|
|
created_by=created_by,
|
|
)
|
|
session.add(api_key)
|
|
return api_key
|