sysnet-auth 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,266 @@
1
+ Metadata-Version: 2.4
2
+ Name: sysnet-auth
3
+ Version: 0.2.0
4
+ Summary: Sdilena autentizacni knihovna pro FastAPI mikrosluzby s Keycloack (OIDC/JWT).
5
+ Author: SYSNET s.r.o.
6
+ License: Proprietary
7
+ Project-URL: Homepage, https://sysnet.cz
8
+ Keywords: fastapi,keycloak,jwt,oidc,auth,sysnet
9
+ Classifier: Framework :: FastAPI
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Programming Language :: Python :: 3.14
14
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
15
+ Classifier: Topic :: Security
16
+ Classifier: Typing :: Typed
17
+ Requires-Python: >=3.11
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: fastapi>=0.115
20
+ Requires-Dist: pydantic>=2.9
21
+ Requires-Dist: pydantic-settings>=2.3
22
+ Requires-Dist: pyjwt[crypto]>=2.9
23
+ Requires-Dist: httpx>=0.27
24
+ Requires-Dist: sysnet-pyutils>=0.1
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=8.0; extra == "dev"
27
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
28
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
29
+ Requires-Dist: cryptography>=42.0; extra == "dev"
30
+ Requires-Dist: mypy>=1.11; extra == "dev"
31
+ Requires-Dist: ruff>=0.6; extra == "dev"
32
+
33
+ # sysnet-auth (import path: `auth_lib`)
34
+
35
+ Sdílená autentizační knihovna pro FastAPI mikroslužby, které ověřují identitu uživatelů přes **Keycloak** (OIDC / JWT). Součást SYSNET ekosystému.
36
+
37
+ Knihovna je záměrně úzká: neobsahuje business logiku, neukládá stav a nepřeposílá tokeny. Jediná zodpovědnost je **validace JWT** a vystavení objektu `AuthenticatedUser` dependency injection mechanismem FastAPI.
38
+
39
+ ---
40
+
41
+ ## Proč existuje
42
+
43
+ V architektuře desítek mikroslužeb nechceme, aby každá z nich měla vlastní implementaci validace JWT. Rozkol v drobných detailech (kontrola audience, leeway, cachování JWKS) je snadný způsob, jak vyrobit bezpečnostní díru. `auth_lib` je **jediné místo, kde se validuje identita uživatele** — a všechny služby ji používají stejně.
44
+
45
+ ---
46
+
47
+ ## Instalace
48
+
49
+ Balíček je publikovaný jako **`sysnet-auth`**, importuje se jako **`auth_lib`**
50
+ (běžný pattern: distribuční jméno ≠ import jméno).
51
+
52
+ ```bash
53
+ pip install sysnet-auth
54
+ ```
55
+
56
+ ```python
57
+ from auth_lib import get_current_user, require_role
58
+ ```
59
+
60
+ ### Python
61
+
62
+ Vyžaduje **Python ≥ 3.11**, plně testováno proti **3.11 – 3.14**.
63
+
64
+ ### Runtime závislosti
65
+
66
+ | Balíček | Role |
67
+ |---------------------|------------------------------------------------|
68
+ | `fastapi` | DI / exception handlery |
69
+ | `pydantic` v2 | modely, validace |
70
+ | `pydantic-settings` | konfigurace přes env |
71
+ | `pyjwt[crypto]` | dekódování + validace JWT |
72
+ | `httpx` | async HTTP klient pro JWKS endpoint |
73
+ | `sysnet-pyutils` | sdílené SYSNET modely (`UserType`, `ErrorModel`, `Log`) |
74
+
75
+ > **Volba JWT knihovny.** `python-jose` je od 2021 neudržovaný. Používáme **PyJWT** — aktivně udržovaný de facto standard pro JWT v Pythonu.
76
+
77
+ ---
78
+
79
+ ## Konfigurace
80
+
81
+ Všechno přes environment proměnné s prefixem `AUTH_`. Pydantic-settings načte settings při prvním volání `get_settings()` a cachuje je na proces.
82
+
83
+ | Proměnná | Default | Popis |
84
+ |-------------------------------------|--------------|-------|
85
+ | `AUTH_KEYCLOAK_URL` | — | Základní URL Keycloaku (`https://kc.example.cz`) |
86
+ | `AUTH_REALM` | — | Název Keycloak realm |
87
+ | `AUTH_AUDIENCE` | — | Očekávaný `aud` claim (typicky `client_id` API) |
88
+ | `AUTH_ALGORITHMS` | `["RS256"]` | Povolené podpisové algoritmy (CSV nebo JSON) |
89
+ | `AUTH_JWKS_CACHE_SECONDS` | `300` | TTL JWKS cache |
90
+ | `AUTH_JWKS_HTTP_TIMEOUT_SECONDS` | `5.0` | HTTP timeout pro fetch JWKS |
91
+ | `AUTH_LEEWAY_SECONDS` | `0` | Tolerance hodinového rozdílu (clock skew) |
92
+ | `AUTH_RESOURCE_CLIENT` | `None` | Pokud nastaveno, mergne i `resource_access.<klient>.roles` |
93
+ | `AUTH_KID_MISS_REFRESH_COOLDOWN_SECONDS` | `0` | Opt-in: refresh při neznámém `kid` 1× za N sekund |
94
+
95
+ ### Odvozené hodnoty
96
+
97
+ - **Issuer:** `{AUTH_KEYCLOAK_URL}/realms/{AUTH_REALM}`
98
+ - **JWKS URL:** `{issuer}/protocol/openid-connect/certs`
99
+
100
+ ---
101
+
102
+ ## Použití ve FastAPI
103
+
104
+ ### Ověřený uživatel
105
+
106
+ ```python
107
+ from fastapi import Depends, FastAPI
108
+ from auth_lib import AuthenticatedUser, get_current_user, install_exception_handlers
109
+
110
+ app = FastAPI()
111
+ install_exception_handlers(app)
112
+
113
+ @app.get("/me")
114
+ async def me(user: AuthenticatedUser = Depends(get_current_user)) -> AuthenticatedUser:
115
+ return user
116
+ ```
117
+
118
+ ### Role guard
119
+
120
+ ```python
121
+ from auth_lib import require_role, require_any_role, require_all_roles
122
+
123
+ @app.delete("/users/{id}")
124
+ async def delete_user(
125
+ id: str,
126
+ user: AuthenticatedUser = Depends(require_role("admin")),
127
+ ):
128
+ ...
129
+
130
+ @app.get("/content")
131
+ async def list_content(
132
+ user: AuthenticatedUser = Depends(require_any_role(["editor", "viewer"])),
133
+ ):
134
+ ...
135
+ ```
136
+
137
+ ### Konverze do SYSNET `UserType`
138
+
139
+ ```python
140
+ from auth_lib import get_current_user
141
+
142
+ @app.get("/user-profile")
143
+ async def profile(user = Depends(get_current_user)):
144
+ sysnet_user = user.to_user_type() # sysnet_pyutils.UserType
145
+ return sysnet_user
146
+ ```
147
+
148
+ Mapping: `sub → identifier`, `preferred_username → name`, `email → email`, `given_name → name_first`, `family_name → name_last`, `name → name_full`.
149
+
150
+ ---
151
+
152
+ ## Observability
153
+
154
+ Knihovna neví, co je Prometheus / OpenTelemetry / strukturovaný log. Místo toho exponuje **pub-sub hooky**, které si konzument zaregistruje:
155
+
156
+ ```python
157
+ from auth_lib import on_token_validated, on_token_rejected, on_jwks_refresh
158
+
159
+ @on_token_validated
160
+ def _ok(user):
161
+ metrics.incr("auth.ok", tags={"sub": user.sub})
162
+
163
+ @on_token_rejected
164
+ def _err(exc):
165
+ metrics.incr("auth.err", tags={"type": type(exc).__name__})
166
+
167
+ @on_jwks_refresh
168
+ def _jwks(n):
169
+ metrics.gauge("auth.jwks.keys", n)
170
+ ```
171
+
172
+ Výjimka v hooku **nikdy neshodí validaci** (hooky jsou best-effort).
173
+
174
+ ### Rychlé zapnutí přes SYSNET logger
175
+
176
+ ```python
177
+ from auth_lib import install_sysnet_logging
178
+ install_sysnet_logging() # zapíše INFO/WARNING přes sysnet_pyutils.Log
179
+ ```
180
+
181
+ Idempotentní — druhé volání už hooky znovu neregistruje.
182
+
183
+ ---
184
+
185
+ ## Výjimky
186
+
187
+ | Výjimka | HTTP | Kdy |
188
+ |--------------------------|------|---------------------------------------------------------|
189
+ | `InvalidTokenError` | 401 | špatný podpis, expirace, `iss`, `aud`, neznámý `kid`, … |
190
+ | `MissingRoleError` | 403 | uživatel je ověřen, ale chybí mu role |
191
+ | `AuthConfigurationError` | 500 | nedostupný JWKS, špatné URL, malformed response |
192
+
193
+ Všechny dědí z `AuthError`.
194
+
195
+ `install_exception_handlers(app)` registruje handler, který vrací **`sysnet_pyutils.ErrorModel`**:
196
+
197
+ ```json
198
+ {"code": 401, "message": "Token has expired"}
199
+ ```
200
+
201
+ Jednotný formát chyb napříč SYSNET službami.
202
+
203
+ ---
204
+
205
+ ## JWKS caching
206
+
207
+ - In-memory TTL cache (default 300 s).
208
+ - Lazy fetch při prvním volání.
209
+ - **Single-flight:** souběh N requestů při cold/expired cache spustí právě jeden fetch.
210
+ - **Fresh cache je autoritativní** — kid miss = `InvalidTokenError`. Brání DoS přes náhodné kidy.
211
+ - **Opt-in cooldown refresh** (`AUTH_KID_MISS_REFRESH_COOLDOWN_SECONDS > 0`): při kid miss ve fresh cache povolí 1 refresh za N sekund — responzivnější reakce na rotaci klíčů.
212
+ - Lazy init `asyncio.Lock` — kompatibilní s Pythonem 3.14 (neváže lock na event loop z doby importu).
213
+ - Žádný background task, žádný disk.
214
+
215
+ ---
216
+
217
+ ## Bezpečnostní poznámky
218
+
219
+ - Ověřujeme vždy **podpis, issuer i audience**.
220
+ - Výchozí `leeway=0`. Zapnout jen při doloženém clock skew.
221
+ - Pouze `RS256` výchozí, `none` ani HS256 nepovolujeme bez dobrého důvodu.
222
+ - Čteme **pouze `Authorization: Bearer`** (žádné cookies, žádné `X-*` hlavičky).
223
+ - Tokeny se nelogují. Diagnostika přes `sub` / `jti`.
224
+ - JWKS fetch má timeout → nedostupný Keycloak vrátí 500, ne čekání donekonečna.
225
+
226
+ ---
227
+
228
+ ## Struktura projektu
229
+
230
+ ```
231
+ auth_lib/
232
+ ├── auth_lib/
233
+ │ ├── __init__.py # veřejné re-exporty (__all__)
234
+ │ ├── config.py # AuthSettings
235
+ │ ├── exceptions.py # AuthError + to_error_model()
236
+ │ ├── models.py # AuthenticatedUser + to_user_type()
237
+ │ ├── jwks.py # async JWKS cache + cooldown
238
+ │ ├── dependencies.py # get_current_user, install_exception_handlers
239
+ │ ├── roles.py # require_role / require_any_role / require_all_roles
240
+ │ ├── observability.py # hook registry + install_sysnet_logging()
241
+ │ └── py.typed # PEP 561 marker
242
+ ├── tests/
243
+ ├── pyproject.toml # sysnet-auth, Python 3.11-3.14
244
+ ├── CHANGELOG.md
245
+ ├── README.md
246
+ └── .gitignore
247
+ ```
248
+
249
+ ---
250
+
251
+ ## Testování
252
+
253
+ ```bash
254
+ pip install -e ".[dev]"
255
+ pytest --cov=auth_lib --cov-report=term-missing
256
+ ```
257
+
258
+ Testy nevolají reálný Keycloak — používají vlastní RSA keypair a JWKS cache předvyplněnou odpovídajícím JWK. **58 testů, 98 % pokrytí.**
259
+
260
+ ---
261
+
262
+ ## Architektonický princip
263
+
264
+ > **Tato knihovna je jediným místem, kde se řeší validace identity uživatele.**
265
+
266
+ Všechny FastAPI mikroslužby ji používají jednotným způsobem. Pokud narazíš na potřebu obejít `auth_lib` (vlastní dekódování, custom validace), je to signál k úpravě `auth_lib` — ne k duplikaci logiky.
@@ -0,0 +1,234 @@
1
+ # sysnet-auth (import path: `auth_lib`)
2
+
3
+ Sdílená autentizační knihovna pro FastAPI mikroslužby, které ověřují identitu uživatelů přes **Keycloak** (OIDC / JWT). Součást SYSNET ekosystému.
4
+
5
+ Knihovna je záměrně úzká: neobsahuje business logiku, neukládá stav a nepřeposílá tokeny. Jediná zodpovědnost je **validace JWT** a vystavení objektu `AuthenticatedUser` dependency injection mechanismem FastAPI.
6
+
7
+ ---
8
+
9
+ ## Proč existuje
10
+
11
+ V architektuře desítek mikroslužeb nechceme, aby každá z nich měla vlastní implementaci validace JWT. Rozkol v drobných detailech (kontrola audience, leeway, cachování JWKS) je snadný způsob, jak vyrobit bezpečnostní díru. `auth_lib` je **jediné místo, kde se validuje identita uživatele** — a všechny služby ji používají stejně.
12
+
13
+ ---
14
+
15
+ ## Instalace
16
+
17
+ Balíček je publikovaný jako **`sysnet-auth`**, importuje se jako **`auth_lib`**
18
+ (běžný pattern: distribuční jméno ≠ import jméno).
19
+
20
+ ```bash
21
+ pip install sysnet-auth
22
+ ```
23
+
24
+ ```python
25
+ from auth_lib import get_current_user, require_role
26
+ ```
27
+
28
+ ### Python
29
+
30
+ Vyžaduje **Python ≥ 3.11**, plně testováno proti **3.11 – 3.14**.
31
+
32
+ ### Runtime závislosti
33
+
34
+ | Balíček | Role |
35
+ |---------------------|------------------------------------------------|
36
+ | `fastapi` | DI / exception handlery |
37
+ | `pydantic` v2 | modely, validace |
38
+ | `pydantic-settings` | konfigurace přes env |
39
+ | `pyjwt[crypto]` | dekódování + validace JWT |
40
+ | `httpx` | async HTTP klient pro JWKS endpoint |
41
+ | `sysnet-pyutils` | sdílené SYSNET modely (`UserType`, `ErrorModel`, `Log`) |
42
+
43
+ > **Volba JWT knihovny.** `python-jose` je od 2021 neudržovaný. Používáme **PyJWT** — aktivně udržovaný de facto standard pro JWT v Pythonu.
44
+
45
+ ---
46
+
47
+ ## Konfigurace
48
+
49
+ Všechno přes environment proměnné s prefixem `AUTH_`. Pydantic-settings načte settings při prvním volání `get_settings()` a cachuje je na proces.
50
+
51
+ | Proměnná | Default | Popis |
52
+ |-------------------------------------|--------------|-------|
53
+ | `AUTH_KEYCLOAK_URL` | — | Základní URL Keycloaku (`https://kc.example.cz`) |
54
+ | `AUTH_REALM` | — | Název Keycloak realm |
55
+ | `AUTH_AUDIENCE` | — | Očekávaný `aud` claim (typicky `client_id` API) |
56
+ | `AUTH_ALGORITHMS` | `["RS256"]` | Povolené podpisové algoritmy (CSV nebo JSON) |
57
+ | `AUTH_JWKS_CACHE_SECONDS` | `300` | TTL JWKS cache |
58
+ | `AUTH_JWKS_HTTP_TIMEOUT_SECONDS` | `5.0` | HTTP timeout pro fetch JWKS |
59
+ | `AUTH_LEEWAY_SECONDS` | `0` | Tolerance hodinového rozdílu (clock skew) |
60
+ | `AUTH_RESOURCE_CLIENT` | `None` | Pokud nastaveno, mergne i `resource_access.<klient>.roles` |
61
+ | `AUTH_KID_MISS_REFRESH_COOLDOWN_SECONDS` | `0` | Opt-in: refresh při neznámém `kid` 1× za N sekund |
62
+
63
+ ### Odvozené hodnoty
64
+
65
+ - **Issuer:** `{AUTH_KEYCLOAK_URL}/realms/{AUTH_REALM}`
66
+ - **JWKS URL:** `{issuer}/protocol/openid-connect/certs`
67
+
68
+ ---
69
+
70
+ ## Použití ve FastAPI
71
+
72
+ ### Ověřený uživatel
73
+
74
+ ```python
75
+ from fastapi import Depends, FastAPI
76
+ from auth_lib import AuthenticatedUser, get_current_user, install_exception_handlers
77
+
78
+ app = FastAPI()
79
+ install_exception_handlers(app)
80
+
81
+ @app.get("/me")
82
+ async def me(user: AuthenticatedUser = Depends(get_current_user)) -> AuthenticatedUser:
83
+ return user
84
+ ```
85
+
86
+ ### Role guard
87
+
88
+ ```python
89
+ from auth_lib import require_role, require_any_role, require_all_roles
90
+
91
+ @app.delete("/users/{id}")
92
+ async def delete_user(
93
+ id: str,
94
+ user: AuthenticatedUser = Depends(require_role("admin")),
95
+ ):
96
+ ...
97
+
98
+ @app.get("/content")
99
+ async def list_content(
100
+ user: AuthenticatedUser = Depends(require_any_role(["editor", "viewer"])),
101
+ ):
102
+ ...
103
+ ```
104
+
105
+ ### Konverze do SYSNET `UserType`
106
+
107
+ ```python
108
+ from auth_lib import get_current_user
109
+
110
+ @app.get("/user-profile")
111
+ async def profile(user = Depends(get_current_user)):
112
+ sysnet_user = user.to_user_type() # sysnet_pyutils.UserType
113
+ return sysnet_user
114
+ ```
115
+
116
+ Mapping: `sub → identifier`, `preferred_username → name`, `email → email`, `given_name → name_first`, `family_name → name_last`, `name → name_full`.
117
+
118
+ ---
119
+
120
+ ## Observability
121
+
122
+ Knihovna neví, co je Prometheus / OpenTelemetry / strukturovaný log. Místo toho exponuje **pub-sub hooky**, které si konzument zaregistruje:
123
+
124
+ ```python
125
+ from auth_lib import on_token_validated, on_token_rejected, on_jwks_refresh
126
+
127
+ @on_token_validated
128
+ def _ok(user):
129
+ metrics.incr("auth.ok", tags={"sub": user.sub})
130
+
131
+ @on_token_rejected
132
+ def _err(exc):
133
+ metrics.incr("auth.err", tags={"type": type(exc).__name__})
134
+
135
+ @on_jwks_refresh
136
+ def _jwks(n):
137
+ metrics.gauge("auth.jwks.keys", n)
138
+ ```
139
+
140
+ Výjimka v hooku **nikdy neshodí validaci** (hooky jsou best-effort).
141
+
142
+ ### Rychlé zapnutí přes SYSNET logger
143
+
144
+ ```python
145
+ from auth_lib import install_sysnet_logging
146
+ install_sysnet_logging() # zapíše INFO/WARNING přes sysnet_pyutils.Log
147
+ ```
148
+
149
+ Idempotentní — druhé volání už hooky znovu neregistruje.
150
+
151
+ ---
152
+
153
+ ## Výjimky
154
+
155
+ | Výjimka | HTTP | Kdy |
156
+ |--------------------------|------|---------------------------------------------------------|
157
+ | `InvalidTokenError` | 401 | špatný podpis, expirace, `iss`, `aud`, neznámý `kid`, … |
158
+ | `MissingRoleError` | 403 | uživatel je ověřen, ale chybí mu role |
159
+ | `AuthConfigurationError` | 500 | nedostupný JWKS, špatné URL, malformed response |
160
+
161
+ Všechny dědí z `AuthError`.
162
+
163
+ `install_exception_handlers(app)` registruje handler, který vrací **`sysnet_pyutils.ErrorModel`**:
164
+
165
+ ```json
166
+ {"code": 401, "message": "Token has expired"}
167
+ ```
168
+
169
+ Jednotný formát chyb napříč SYSNET službami.
170
+
171
+ ---
172
+
173
+ ## JWKS caching
174
+
175
+ - In-memory TTL cache (default 300 s).
176
+ - Lazy fetch při prvním volání.
177
+ - **Single-flight:** souběh N requestů při cold/expired cache spustí právě jeden fetch.
178
+ - **Fresh cache je autoritativní** — kid miss = `InvalidTokenError`. Brání DoS přes náhodné kidy.
179
+ - **Opt-in cooldown refresh** (`AUTH_KID_MISS_REFRESH_COOLDOWN_SECONDS > 0`): při kid miss ve fresh cache povolí 1 refresh za N sekund — responzivnější reakce na rotaci klíčů.
180
+ - Lazy init `asyncio.Lock` — kompatibilní s Pythonem 3.14 (neváže lock na event loop z doby importu).
181
+ - Žádný background task, žádný disk.
182
+
183
+ ---
184
+
185
+ ## Bezpečnostní poznámky
186
+
187
+ - Ověřujeme vždy **podpis, issuer i audience**.
188
+ - Výchozí `leeway=0`. Zapnout jen při doloženém clock skew.
189
+ - Pouze `RS256` výchozí, `none` ani HS256 nepovolujeme bez dobrého důvodu.
190
+ - Čteme **pouze `Authorization: Bearer`** (žádné cookies, žádné `X-*` hlavičky).
191
+ - Tokeny se nelogují. Diagnostika přes `sub` / `jti`.
192
+ - JWKS fetch má timeout → nedostupný Keycloak vrátí 500, ne čekání donekonečna.
193
+
194
+ ---
195
+
196
+ ## Struktura projektu
197
+
198
+ ```
199
+ auth_lib/
200
+ ├── auth_lib/
201
+ │ ├── __init__.py # veřejné re-exporty (__all__)
202
+ │ ├── config.py # AuthSettings
203
+ │ ├── exceptions.py # AuthError + to_error_model()
204
+ │ ├── models.py # AuthenticatedUser + to_user_type()
205
+ │ ├── jwks.py # async JWKS cache + cooldown
206
+ │ ├── dependencies.py # get_current_user, install_exception_handlers
207
+ │ ├── roles.py # require_role / require_any_role / require_all_roles
208
+ │ ├── observability.py # hook registry + install_sysnet_logging()
209
+ │ └── py.typed # PEP 561 marker
210
+ ├── tests/
211
+ ├── pyproject.toml # sysnet-auth, Python 3.11-3.14
212
+ ├── CHANGELOG.md
213
+ ├── README.md
214
+ └── .gitignore
215
+ ```
216
+
217
+ ---
218
+
219
+ ## Testování
220
+
221
+ ```bash
222
+ pip install -e ".[dev]"
223
+ pytest --cov=auth_lib --cov-report=term-missing
224
+ ```
225
+
226
+ Testy nevolají reálný Keycloak — používají vlastní RSA keypair a JWKS cache předvyplněnou odpovídajícím JWK. **58 testů, 98 % pokrytí.**
227
+
228
+ ---
229
+
230
+ ## Architektonický princip
231
+
232
+ > **Tato knihovna je jediným místem, kde se řeší validace identity uživatele.**
233
+
234
+ Všechny FastAPI mikroslužby ji používají jednotným způsobem. Pokud narazíš na potřebu obejít `auth_lib` (vlastní dekódování, custom validace), je to signál k úpravě `auth_lib` — ne k duplikaci logiky.
@@ -0,0 +1,74 @@
1
+ """
2
+ auth_lib -- sdilena autentizacni knihovna pro FastAPI mikrosluzby s Keycloackem.
3
+
4
+ Distribuce (PyPi): ``sysnet-auth``. Import: ``auth_lib``.
5
+ Python >= 3.11 (testovano az do 3.14).
6
+
7
+ Verzovani:
8
+ Jediny zdroj pravdy je ``version`` v ``pyproject.toml``. Runtime verze
9
+ se cte pres ``importlib.metadata``, aby nedochazelo k drift mezi nimi.
10
+ """
11
+
12
+ from importlib.metadata import PackageNotFoundError, version as _pkg_version
13
+
14
+ from auth_lib.config import AuthSettings, get_settings
15
+ from auth_lib.dependencies import (
16
+ get_current_user,
17
+ install_exception_handlers,
18
+ )
19
+ from auth_lib.exceptions import (
20
+ AuthConfigurationError,
21
+ AuthError,
22
+ InvalidTokenError,
23
+ MissingRoleError,
24
+ )
25
+ from auth_lib.jwks import JWKSClient, get_jwks_client, reset_jwks_client
26
+ from auth_lib.models import AuthenticatedUser
27
+ from auth_lib.observability import (
28
+ clear_listeners,
29
+ install_sysnet_logging,
30
+ on_jwks_refresh,
31
+ on_token_rejected,
32
+ on_token_validated,
33
+ remove_listener,
34
+ )
35
+ from auth_lib.roles import require_all_roles, require_any_role, require_role
36
+
37
+ try:
38
+ __version__: str = _pkg_version("sysnet-auth")
39
+ except PackageNotFoundError:
40
+ # Editable checkout bez instalace (napr. v nekterych CI fazich nebo
41
+ # primy PYTHONPATH). Nezastavujeme import, jen signalizujeme "dev".
42
+ __version__ = "0.0.0+dev"
43
+
44
+ __all__ = [
45
+ # modely a vyjimky
46
+ "AuthenticatedUser",
47
+ "AuthError",
48
+ "AuthConfigurationError",
49
+ "InvalidTokenError",
50
+ "MissingRoleError",
51
+ # konfigurace
52
+ "AuthSettings",
53
+ "get_settings",
54
+ # FastAPI integrace
55
+ "get_current_user",
56
+ "install_exception_handlers",
57
+ # role guards
58
+ "require_role",
59
+ "require_all_roles",
60
+ "require_any_role",
61
+ # JWKS
62
+ "JWKSClient",
63
+ "get_jwks_client",
64
+ "reset_jwks_client",
65
+ # observability
66
+ "on_token_validated",
67
+ "on_token_rejected",
68
+ "on_jwks_refresh",
69
+ "remove_listener",
70
+ "clear_listeners",
71
+ "install_sysnet_logging",
72
+ # metadata
73
+ "__version__",
74
+ ]
@@ -0,0 +1,114 @@
1
+ """
2
+ Konfigurace knihovny pres environment promenne.
3
+
4
+ Prefix: ``AUTH_``. Pouziva pydantic-settings (pydantic v2).
5
+ Python >= 3.11 (testovano az 3.14).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from functools import lru_cache
11
+
12
+ from pydantic import Field, field_validator
13
+ from pydantic_settings import BaseSettings, SettingsConfigDict
14
+
15
+
16
+ class AuthSettings(BaseSettings):
17
+ """Konfigurace auth_lib."""
18
+
19
+ model_config = SettingsConfigDict(
20
+ env_prefix="AUTH_",
21
+ env_file=None,
22
+ case_sensitive=False,
23
+ extra="ignore",
24
+ # Vypne implicitni JSON dekodovani env stringu u kolekci -- chceme si
25
+ # parsing AUTH_ALGORITHMS ridit sami (podporujeme CSV i JSON).
26
+ enable_decoding=False,
27
+ )
28
+
29
+ keycloak_url: str = Field(
30
+ ...,
31
+ description="Zakladni URL Keycloaku.",
32
+ )
33
+ realm: str = Field(
34
+ ...,
35
+ description="Nazev Keycloak realm.",
36
+ )
37
+ audience: str = Field(
38
+ ...,
39
+ description="Ocekavana hodnota aud claimu.",
40
+ )
41
+ algorithms: list[str] = Field(
42
+ default_factory=lambda: ["RS256"],
43
+ description="Povolene podepisovaci algoritmy.",
44
+ )
45
+ jwks_cache_seconds: int = Field(
46
+ default=300,
47
+ ge=1,
48
+ description="TTL JWKS cache v sekundach.",
49
+ )
50
+ jwks_http_timeout_seconds: float = Field(
51
+ default=5.0,
52
+ gt=0,
53
+ description="HTTP timeout pro fetch JWKS endpointu.",
54
+ )
55
+ leeway_seconds: int = Field(
56
+ default=0,
57
+ ge=0,
58
+ description="Tolerance hodin (clock skew).",
59
+ )
60
+
61
+ resource_client: str | None = Field(
62
+ default=None,
63
+ description=(
64
+ "Nepovinne: pokud je uveden, AuthenticatedUser bude obsahovat i role "
65
+ "z resource_access.<resource_client>.roles (Keycloak client-level role)."
66
+ ),
67
+ )
68
+
69
+ kid_miss_refresh_cooldown_seconds: int = Field(
70
+ default=0,
71
+ ge=0,
72
+ description=(
73
+ "Opt-in: pokud >0, pri kid miss ve fresh JWKS cache povolime 1 refresh "
74
+ "za tento pocet sekund (responzivnejsi reakce na rotaci klicu v Keycloaku "
75
+ "s kontrolovanym DoS riskem). Default 0 = vypnuto."
76
+ ),
77
+ )
78
+
79
+ @field_validator("keycloak_url")
80
+ @classmethod
81
+ def _strip_trailing_slash(cls, v: str) -> str:
82
+ return v.rstrip("/")
83
+
84
+ @field_validator("algorithms", mode="before")
85
+ @classmethod
86
+ def _parse_algorithms(cls, v: object) -> object:
87
+ # AUTH_ALGORITHMS=RS256,RS512 nebo AUTH_ALGORITHMS=["RS256","RS512"]
88
+ if isinstance(v, str):
89
+ s = v.strip()
90
+ if s.startswith("["):
91
+ import json
92
+ try:
93
+ parsed = json.loads(s)
94
+ if isinstance(parsed, list):
95
+ return [str(x).strip() for x in parsed if str(x).strip()]
96
+ except json.JSONDecodeError:
97
+ pass
98
+ parts = [p.strip().strip('"').strip("'") for p in s.split(",")]
99
+ return [p for p in parts if p]
100
+ return v
101
+
102
+ @property
103
+ def issuer(self) -> str:
104
+ return f"{self.keycloak_url}/realms/{self.realm}"
105
+
106
+ @property
107
+ def jwks_url(self) -> str:
108
+ return f"{self.issuer}/protocol/openid-connect/certs"
109
+
110
+
111
+ @lru_cache(maxsize=1)
112
+ def get_settings() -> AuthSettings:
113
+ """Vrati cachovanou instanci settings. Reset pres get_settings.cache_clear()."""
114
+ return AuthSettings() # type: ignore[call-arg]