- Postgres в compose; пакет db (SQLAlchemy async): clients, windows, requests. - Все эндпоинты, кроме /health, — только с ключом в X-API-Key, иначе 401; без выпущенных ключей доступа нет. В базе — только sha256 ключа. - Задача и окно сессии привязаны к клиенту: чужой ключ их не видит. - Воркер пишет журнал: окно (открыто, последнее обращение, закрыто) и задачу (в очереди, начало, конец, итог, данные без секретов). Сбой записи не роняет задачу. - python -m db.cli create|list|revoke|stats; make key-create/key-list/key-revoke/stats. - smoke и loadtest с ключом. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9dc2c0b619
commit
1a4bd8d672
34 files changed
+857
-81
No files matched your search
@@ -0,0 +1,48 @@
|
||||
"""Клиенты и API-ключи (#4). Ключ показывается один раз при выпуске, в базе — только sha256."""
|
||||
import hashlib
|
||||
import secrets
|
||||
|
||||
from sqlalchemy import select, update
|
||||
|
||||
from db.engine import Database
|
||||
from db.models import Client, utcnow
|
||||
|
||||
KEY_PREFIX = "rbx_"
|
||||
|
||||
|
||||
def hash_key(key: str) -> str:
|
||||
return hashlib.sha256(key.encode()).hexdigest()
|
||||
|
||||
|
||||
async def create_client(db: Database, name: str) -> tuple[Client, str]:
|
||||
key = KEY_PREFIX + secrets.token_urlsafe(32)
|
||||
client = Client(name=name, key_hash=hash_key(key), key_prefix=key[:12])
|
||||
async with db.session() as session, session.begin():
|
||||
session.add(client)
|
||||
return client, key
|
||||
|
||||
|
||||
async def authenticate(db: Database, key: str | None) -> Client | None:
|
||||
"""Клиент по ключу, если ключ не отозван; отмечает время последнего обращения."""
|
||||
if not key:
|
||||
return None
|
||||
async with db.session() as session, session.begin():
|
||||
client = await session.scalar(
|
||||
select(Client).where(Client.key_hash == hash_key(key), Client.revoked_at.is_(None))
|
||||
)
|
||||
if client:
|
||||
client.last_access_at = utcnow()
|
||||
return client
|
||||
|
||||
|
||||
async def revoke(db: Database, client_id: int) -> bool:
|
||||
async with db.session() as session, session.begin():
|
||||
result = await session.execute(
|
||||
update(Client).where(Client.id == client_id, Client.revoked_at.is_(None)).values(revoked_at=utcnow())
|
||||
)
|
||||
return result.rowcount > 0
|
||||
|
||||
|
||||
async def list_clients(db: Database) -> list[Client]:
|
||||
async with db.session() as session:
|
||||
return list(await session.scalars(select(Client).order_by(Client.id)))
|
||||
Reference in new issue
Block a user