Metadata-Version: 2.4
Name: sysnet-apikeys
Version: 0.2.0
Summary: SYSNET: správa API klíčů FastAPI služeb (hash, klient s více klíči, rotace s překryvem, scopes, admin API)
Author-email: SYSNET Development Team <info@sysnet.cz>
License-Expression: AGPL-3.0-or-later
Keywords: api-key,fastapi,authentication,rotation,sysnet
Classifier: Framework :: FastAPI
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
Classifier: Topic :: Security
Classifier: Typing :: Typed
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: sysnet-pyutils>=1.7.8
Requires-Dist: PyYAML>=6.0.1
Requires-Dist: pydantic>=2.10.6
Provides-Extra: fastapi
Requires-Dist: fastapi>=0.115; extra == "fastapi"
Provides-Extra: sql
Requires-Dist: sqlalchemy[asyncio]>=2.0; extra == "sql"
Requires-Dist: asyncpg>=0.30; extra == "sql"
Provides-Extra: mongo
Requires-Dist: pymongo>=4.9; extra == "mongo"
Provides-Extra: cli
Requires-Dist: typer>=0.12; extra == "cli"
Provides-Extra: dev
Requires-Dist: fastapi>=0.115; extra == "dev"
Requires-Dist: sqlalchemy[asyncio]>=2.0; extra == "dev"
Requires-Dist: asyncpg>=0.30; extra == "dev"
Requires-Dist: pymongo>=4.9; extra == "dev"
Requires-Dist: typer>=0.12; extra == "dev"
Requires-Dist: httpx>=0.27; extra == "dev"
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: pytest-asyncio>=0.26; extra == "dev"
Requires-Dist: ruff>=0.6; extra == "dev"
Requires-Dist: mypy>=1.10; extra == "dev"
Requires-Dist: types-PyYAML; extra == "dev"
Dynamic: license-file

# sysnet-apikeys

Sdílená knihovna pro správu API klíčů FastAPI služeb SYSNET. Rozhodnutí a zdůvodnění: ADR 006 v CML
(`docs/adr/006-sdilena-knihovna-sysnet-apikeys.md`).

**Stav: 0.2.0.** Jádro, úložiště (paměť, SQL, Mongo, starý YAML jen pro čtení), vazba na FastAPI, admin router a CLI.
Není zatím nasazeno v žádné službě (pilot: `taxonomy`).

## Co řeší

- Klíč se ukládá jako SHA-256, otevřený klíč se vrací jednou při vydání.
- Klient má víc klíčů; **rotace** vydá nový klíč a starému zkrátí platnost na `nyní + grace`. Oba platí souběžně.
- Chybějící, neznámý, zrušený i propadlý klíč dává stejnou chybu (nic se nenapovídá útočníkovi).
- Identita se vrací hodnotou (`Principal`). Nesdílí se stav mezi požadavky, na rozdíl od `Context.check_api_key`
  v `sysnet-pyutils`, který ho v souběhu zaměňuje.
- Nové klíče mají tvar `snk_<public_id>_<tajemství>`; `public_id` je bezpečné uvádět v logu a v tiketu.

## Instalace

```bash
pip install "sysnet-apikeys[fastapi,mongo]"    # nebo [fastapi,sql]; CLI: přidejte cli
```

## Příklad (ověřený spuštěním)

```python
import asyncio
from datetime import timedelta
from typing import Annotated

import httpx
from fastapi import Depends, FastAPI

from sysnet_apikeys import ApiKeyService, InMemoryStore, Principal
from sysnet_apikeys.fastapi import ApiKeyAuth, install_error_handlers

service = ApiKeyService(InMemoryStore())
auth = ApiKeyAuth(service)
app = FastAPI()
install_error_handlers(app)


@app.get("/dokumenty")
async def dokumenty(principal: Annotated[Principal, Depends(auth)]) -> dict[str, str]:
    return {"volá": principal.client}


async def main() -> None:
    old = await service.issue("ippc-docs", scopes=["read"])
    new = await service.rotate("ippc-docs", grace=timedelta(days=1))
    async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://t") as c:
        for key in (old.key, new.key):
            print((await c.get("/dokumenty", headers={"X-API-KEY": key})).status_code)


asyncio.run(main())
```

Výstup: `200` a `200`. Starý i nový klíč platí do konce překryvu.

## Úložiště

| Úložiště | Extra | Poznámka |
|---|---|---|
| `MongoStore(db, collection=...)` | `mongo` | `pymongo.AsyncMongoClient`; `_id` = hash, unikátní `public_id` |
| `SqlStore(engine, table=...)` | `sql` | PostgreSQL (asyncpg); `create_schema()` vytvoří tabulku |
| `LegacyYamlStore` | (jádro) | jen pro čtení, pro migraci |
| `InMemoryStore` | (jádro) | testy a malé nástroje |

## Admin API a CLI

```python
app.include_router(make_admin_router(service, admin_key=os.environ.get("APIKEYS_ADMIN_KEY")), prefix="/admin")
```

Bez `admin_key` vrací admin API 503. CLI čte úložiště z proměnných `APIKEYS_STORE` (`mongo`|`sql`) a
`APIKEYS_MONGO_URL`/`APIKEYS_MONGO_DB` nebo `APIKEYS_SQL_URL`; otevřený klíč tiskne jen `issue` a `rotate`.
`sysnet-apikeys import-yaml conf/x.yml` je dry-run, `--apply` zapíše hashe (klienti se nemění).

## Migrace ze starého YAML

`LegacyYamlStore.from_file("conf/xyz.yml")` čte stávající `api_keys: [{klíč: jméno}]`. Klíče dostanou oprávnění `*`
a `Principal.agenda` a `Principal.client` odpovídají dnešnímu `agenda` a `user_name`. Úložiště je jen pro čtení.

## Vývoj

```bash
python -m venv .venv && .venv/bin/pip install -e ".[dev]"
.venv/bin/python -m pytest && .venv/bin/ruff check . && .venv/bin/mypy
```

Testy úložišť potřebují PostgreSQL a MongoDB (`APIKEYS_TEST_PG`, `APIKEYS_TEST_MONGO`); bez nich **padají**, nepřeskakují se.

Verzování: `pyproject.toml` a `sysnet_apikeys/__init__.py` musí mít stejnou verzi, tag `vX.Y.Z` spustí release.
Licence: AGPL-3.0 (stejně jako ostatní sdílené knihovny SYSNET).
