xtr-security 3.0.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.
Files changed (52) hide show
  1. xtr_security-3.0.0/LICENSE +21 -0
  2. xtr_security-3.0.0/PKG-INFO +398 -0
  3. xtr_security-3.0.0/README.md +370 -0
  4. xtr_security-3.0.0/pyproject.toml +165 -0
  5. xtr_security-3.0.0/pyproject.toml.orig +145 -0
  6. xtr_security-3.0.0/src/xtr_security/.agents/skills/xtr-security/SKILL.md +291 -0
  7. xtr_security-3.0.0/src/xtr_security/.agents/skills/xtr-security/references/authorization.md +149 -0
  8. xtr_security-3.0.0/src/xtr_security/.agents/skills/xtr-security/references/firewalls-and-authenticators.md +165 -0
  9. xtr_security-3.0.0/src/xtr_security/.agents/skills/xtr-security/references/password-hashing.md +122 -0
  10. xtr_security-3.0.0/src/xtr_security/__init__.py +13 -0
  11. xtr_security-3.0.0/src/xtr_security/access_token/__init__.py +19 -0
  12. xtr_security-3.0.0/src/xtr_security/access_token/add_token_handler_factory.py +35 -0
  13. xtr_security-3.0.0/src/xtr_security/access_token/oidc_token_handler_factory.py +114 -0
  14. xtr_security-3.0.0/src/xtr_security/access_token/service_token_handler_factory.py +48 -0
  15. xtr_security-3.0.0/src/xtr_security/access_token/token_handler_factory_interface.py +52 -0
  16. xtr_security-3.0.0/src/xtr_security/bundle/__init__.py +97 -0
  17. xtr_security-3.0.0/src/xtr_security/bundle/_firewall_scheme_middleware.py +38 -0
  18. xtr_security-3.0.0/src/xtr_security/bundle/_firewall_scheme_middleware_factory.py +33 -0
  19. xtr_security-3.0.0/src/xtr_security/bundle/_password_hasher_build.py +83 -0
  20. xtr_security-3.0.0/src/xtr_security/bundle/_wiring.py +736 -0
  21. xtr_security-3.0.0/src/xtr_security/bundle/access_control_configs.py +83 -0
  22. xtr_security-3.0.0/src/xtr_security/bundle/access_decision_manager_config.py +66 -0
  23. xtr_security-3.0.0/src/xtr_security/bundle/authenticator_configs.py +56 -0
  24. xtr_security-3.0.0/src/xtr_security/bundle/firewall_dispatcher_name.py +19 -0
  25. xtr_security-3.0.0/src/xtr_security/bundle/make_firewalls_event_dispatcher_traceable_pass.py +53 -0
  26. xtr_security-3.0.0/src/xtr_security/bundle/password_hasher_configs.py +166 -0
  27. xtr_security-3.0.0/src/xtr_security/bundle/register_global_security_event_listeners_pass.py +69 -0
  28. xtr_security-3.0.0/src/xtr_security/bundle/security_bundle.py +305 -0
  29. xtr_security-3.0.0/src/xtr_security/bundle/security_config.py +155 -0
  30. xtr_security-3.0.0/src/xtr_security/bundle/token_handler_configs.py +120 -0
  31. xtr_security-3.0.0/src/xtr_security/bundle/user_provider_configs.py +98 -0
  32. xtr_security-3.0.0/src/xtr_security/command/__init__.py +15 -0
  33. xtr_security-3.0.0/src/xtr_security/command/debug_firewall_command.py +76 -0
  34. xtr_security-3.0.0/src/xtr_security/exception/__init__.py +12 -0
  35. xtr_security-3.0.0/src/xtr_security/exception/invalid_configuration_error.py +20 -0
  36. xtr_security-3.0.0/src/xtr_security/factory/__init__.py +17 -0
  37. xtr_security-3.0.0/src/xtr_security/factory/access_token_factory.py +220 -0
  38. xtr_security-3.0.0/src/xtr_security/factory/add_authenticator_factory.py +36 -0
  39. xtr_security-3.0.0/src/xtr_security/factory/authenticator_factory_interface.py +68 -0
  40. xtr_security-3.0.0/src/xtr_security/firewall_config.py +107 -0
  41. xtr_security-3.0.0/src/xtr_security/firewall_context.py +68 -0
  42. xtr_security-3.0.0/src/xtr_security/firewall_map.py +93 -0
  43. xtr_security-3.0.0/src/xtr_security/py.typed +0 -0
  44. xtr_security-3.0.0/src/xtr_security/recipe/__init__.py +16 -0
  45. xtr_security-3.0.0/src/xtr_security/recipe/manifest.toml +30 -0
  46. xtr_security-3.0.0/src/xtr_security/security.py +108 -0
  47. xtr_security-3.0.0/src/xtr_security/user_provider/__init__.py +21 -0
  48. xtr_security-3.0.0/src/xtr_security/user_provider/add_user_provider_factory.py +35 -0
  49. xtr_security-3.0.0/src/xtr_security/user_provider/chain_factory.py +82 -0
  50. xtr_security-3.0.0/src/xtr_security/user_provider/in_memory_factory.py +68 -0
  51. xtr_security-3.0.0/src/xtr_security/user_provider/service_factory.py +48 -0
  52. xtr_security-3.0.0/src/xtr_security/user_provider/user_provider_factory_interface.py +51 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 xterr
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,398 @@
1
+ Metadata-Version: 2.4
2
+ Name: xtr-security
3
+ Version: 3.0.0
4
+ Summary: The xtr security bundle: the family facade and the container wiring that configures it.
5
+ Keywords: security,authentication,authorization,bundle
6
+ Author: Xterr
7
+ Author-email: Xterr <me@xterr.dev>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Typing :: Typed
17
+ Requires-Dist: xtr-security-core>=3.0,<4
18
+ Requires-Dist: xtr-security-http>=3.0,<4
19
+ Requires-Dist: xtr-password-hasher>=3.0,<4
20
+ Requires-Dist: xtr-dependency-injection>=3.0,<4
21
+ Requires-Dist: xtr-event-dispatcher>=3.0,<4
22
+ Requires-Dist: xtr-http-kernel>=3.0,<4
23
+ Requires-Dist: typing-extensions>=4.12
24
+ Requires-Dist: xtr-console>=3.0,<4 ; extra == 'console'
25
+ Requires-Python: >=3.11
26
+ Provides-Extra: console
27
+ Description-Content-Type: text/markdown
28
+
29
+ <div align="center">
30
+
31
+ # xtr-security
32
+
33
+ **The security bundle for xtr applications: the family facade and the container wiring that configures it.**
34
+
35
+ <img alt="python 3.11+" src="https://img.shields.io/badge/python-%E2%89%A5%203.11-3776AB?logo=python&logoColor=white">
36
+ <img alt="typed" src="https://img.shields.io/badge/typed-ty%20%2B%20basedpyright-1f6feb">
37
+ <img alt="license MIT" src="https://img.shields.io/badge/license-MIT-blue">
38
+
39
+ </div>
40
+
41
+ ---
42
+
43
+ ## Why?
44
+
45
+ The security family is three libraries — [xtr-security-core](../xtr-security-core),
46
+ [xtr-security-http](../xtr-security-http) and [xtr-password-hasher](../xtr-password-hasher) —
47
+ each usable on its own. Wiring them together
48
+ by hand is a lot of moving parts: a token storage per request, voters gathered into a decision
49
+ manager, a user provider and a password hasher, and, per firewall, an authenticator over its own
50
+ event dispatcher, an access map, an entry point and the OpenAPI scheme it shows.
51
+
52
+ This package is the one bundle of the family. An application lists `SecurityBundle`, writes one
53
+ `SecurityConfig`, and the container owns the rest.
54
+
55
+ - 🔥 **Firewalls as configuration** — a slice of the request space, how it authenticates, and
56
+ the rules it enforces, in one frozen value.
57
+ - 🧩 **Open at the seams** — a third-party bundle adds a kind of authenticator, token handler or
58
+ user provider through the factory registries, without this package knowing it.
59
+ - 🪪 **The FastAPI surface stays FastAPI's** — `Firewall`, `IsGranted` and `CurrentUser` are
60
+ dependencies and decorators; the generated OpenAPI schema names each firewall's scheme and each
61
+ route's scopes.
62
+ - 🔒 **Scoped to the request** — the token storage, the authorization checker and the `Security`
63
+ facade are per-request; concurrent requests never share them.
64
+
65
+ ## Install
66
+
67
+ ```sh
68
+ uv add xtr-security # the bundle, wiring core, http and the password hasher
69
+ uv add "xtr-security[console]" # + debug:firewall and security:hash-password
70
+ uv add "xtr-security-http[oidc]" # + verifying third-party OIDC issuers' tokens
71
+ ```
72
+
73
+ Requires Python 3.11+. The core, http-edge and password-hasher libraries come with the package;
74
+ `console` adds the commands. The `oidc` extra of [xtr-security-http](../xtr-security-http) adds
75
+ joserfc and httpx and the OIDC token handler; the bundle registers its factory only when that
76
+ extra is installed. Self-issued JSON Web Tokens are a separate package,
77
+ [xtr-security-jwt](../xtr-security-jwt), which depends on this one and registers itself through
78
+ the [extension points](#extending).
79
+
80
+ ## Quick start
81
+
82
+ Write a token handler that turns a bearer token into a user, configure a firewall that uses it,
83
+ and secure the routes — no JSON Web Tokens needed.
84
+
85
+ ```python
86
+ # app/security_handler.py
87
+ from xtr_dependency_injection import as_service
88
+ from xtr_security_core import InMemoryUser
89
+ from xtr_security_http import UserBadge
90
+ from xtr_security_http.access_token.access_token_handler_interface import (
91
+ AccessTokenHandlerInterface,
92
+ )
93
+ from xtr_security_http.exception import InvalidAccessTokenError
94
+
95
+
96
+ @as_service
97
+ class MyTokenHandler(AccessTokenHandlerInterface):
98
+ async def get_user_badge_from(self, access_token: str) -> UserBadge:
99
+ if access_token != "s3cret":
100
+ raise InvalidAccessTokenError("Unknown token.")
101
+ return UserBadge("alice", user_loader=lambda i: InMemoryUser(i, roles=["ROLE_USER"]))
102
+ ```
103
+
104
+ ```python
105
+ # app/config/security.py
106
+ from xtr_dependency_injection import configure
107
+ from xtr_security.bundle import (
108
+ AccessControlConfig,
109
+ AccessTokenConfig,
110
+ FirewallConfig,
111
+ SecurityConfig,
112
+ ServiceTokenHandlerConfig,
113
+ )
114
+
115
+ from app.security_handler import MyTokenHandler
116
+
117
+
118
+ @configure
119
+ def security() -> SecurityConfig:
120
+ return SecurityConfig(
121
+ firewalls={
122
+ "api": FirewallConfig(
123
+ pattern=r"^/api",
124
+ authenticators=(
125
+ AccessTokenConfig(
126
+ token_handler=ServiceTokenHandlerConfig(MyTokenHandler),
127
+ realm="api",
128
+ ),
129
+ ),
130
+ ),
131
+ },
132
+ access_control=(AccessControlConfig(path=r"^/api", attribute="IS_AUTHENTICATED"),),
133
+ )
134
+ ```
135
+
136
+ ```python
137
+ # app/web.py
138
+ from typing import Annotated
139
+
140
+ from fastapi import FastAPI
141
+ from xtr_dependency_injection import Kernel
142
+ from xtr_http_kernel import setup
143
+ from xtr_security_core.user.user_interface import UserInterface
144
+ from xtr_security_http import CurrentUser, Firewall, IsGranted
145
+
146
+ from app.bundles import BUNDLES
147
+
148
+ api = Firewall("api")
149
+ app = FastAPI(dependencies=[Firewall()])
150
+
151
+
152
+ @app.get("/api/me")
153
+ async def me(user: Annotated[UserInterface, CurrentUser()]) -> dict[str, str]:
154
+ return {"user": user.get_user_identifier()}
155
+
156
+
157
+ @app.get("/api/books", dependencies=[api.scoped("books:read")])
158
+ async def books() -> list[str]: ...
159
+
160
+
161
+ @app.delete("/api/books/{isbn}")
162
+ @IsGranted("ROLE_ADMIN")
163
+ async def delete_book(isbn: str) -> None: ...
164
+
165
+
166
+ kernel = Kernel("app", concurrent_scoped_access=True)
167
+ setup(app, kernel)
168
+ ```
169
+
170
+ ```console
171
+ $ curl -i localhost:8000/api/me
172
+ HTTP/1.1 401 Unauthorized
173
+ WWW-Authenticate: Bearer realm="api"
174
+
175
+ $ curl -i -H 'Authorization: Bearer s3cret' localhost:8000/api/me
176
+ HTTP/1.1 200 OK
177
+
178
+ {"user":"alice"}
179
+ ```
180
+
181
+ ## Configure
182
+
183
+ `SecurityConfig` is a frozen dataclass buildable with no arguments; every field has a default,
184
+ so the zero-config path is no firewalls and the affirmative decision strategy.
185
+
186
+ | Field | Default | What it is |
187
+ |---|---|---|
188
+ | `firewalls` | `{}` | The firewalls, keyed by name, matched in order — first match wins |
189
+ | `providers` | `{}` | The user providers, keyed by the name firewalls refer to |
190
+ | `password_hashers` | `{}` | The hasher each user class, `"module:Class"` string or name is hashed by |
191
+ | `role_hierarchy` | `{}` | The roles each role reaches, expanding a token's roles |
192
+ | `access_control` | `()` | The access-control rules, tried in order |
193
+ | `access_decision_manager` | affirmative | How the voters' answers become one decision |
194
+ | `expose_security_errors` | `NONE` | How much of an authentication failure reaches the client |
195
+ | `trace_votes` | `None` | Announce every vote as an event; `None` follows `kernel.debug` |
196
+ | `authenticator_factories` | built-ins | The factories that build firewalls' authenticators |
197
+ | `token_handler_factories` | built-ins | The factories that build access-token handlers |
198
+ | `user_provider_factories` | built-ins | The factories that build user providers |
199
+
200
+ ### Firewalls
201
+
202
+ `FirewallConfig` claims a slice of the request space and says how it authenticates:
203
+
204
+ | Field | Default | What it is |
205
+ |---|---|---|
206
+ | `pattern` | `None` | A regular expression matched against the request path |
207
+ | `host` | `None` | A regular expression matched against the request host |
208
+ | `methods` | `()` | The HTTP methods claimed; every method when empty |
209
+ | `request_matcher` | `None` | A callable deciding the match, tried last |
210
+ | `security` | `True` | `False` lets every request the firewall claims through untouched |
211
+ | `stateless` | `True` | Only `True` is accepted in this version |
212
+ | `provider` | `None` | The user provider the firewall loads users from |
213
+ | `user_checker` | `None` | A user-checker service type, or the default in-memory checker |
214
+ | `entry_point` | `None` | The authenticator whose challenge answers an unauthenticated request |
215
+ | `access_denied_handler` | `None` | A handler for a denied, fully-authenticated caller |
216
+ | `authenticators` | `()` | The authenticator configurations the firewall runs |
217
+ | `required_badges` | `()` | Badge types every passport must carry |
218
+
219
+ A firewall matched per request — `Firewall()` — runs the first firewall whose matcher claims the
220
+ request; a firewall bound by name — `Firewall("api")` — runs that one and shows its exact scheme
221
+ in OpenAPI. Authentication runs once per request however many firewall dependencies a route
222
+ carries.
223
+
224
+ ### Token handlers
225
+
226
+ An access-token authenticator (`AccessTokenConfig`) names a `token_handler` the bundle builds
227
+ through a factory matched by its type:
228
+
229
+ | Configuration | Key | Verifies |
230
+ |---|---|---|
231
+ | `ServiceTokenHandlerConfig(service)` | `id` | A handler the application registered as a service |
232
+ | `OidcTokenHandlerConfig(issuers, audience, …)` | `oidc` | A third-party OIDC issuer's tokens (needs the `oidc` extra) |
233
+
234
+ `OidcTokenHandlerConfig` takes exactly one key source — `keyset` (a JWKS document), `discovery_uri`
235
+ (an issuer base URI the `jwks_uri` is discovered from) or `jwks_uri` — plus the `issuers` and
236
+ `audience` a valid token must carry; `algorithms` default to `("RS256",)`, `claim` to `"sub"`.
237
+
238
+ ### Access control
239
+
240
+ `AccessControlConfig` is a request pattern and the one attribute a matching request must hold —
241
+ a role, `"IS_AUTHENTICATED"`, `"PUBLIC_ACCESS"`, or an `OAUTH2_SCOPE(...)` string. Rules are
242
+ tried in order, so a narrow `^/api/admin` rule comes before the broad `^/api`.
243
+
244
+ ### Voters
245
+
246
+ The decision manager gathers every voter: the built-in role-hierarchy, authenticated, OAuth2
247
+ scope and closure voters, and every application voter — a class implementing
248
+ `VoterInterface`, autoconfigured with the `security.voter` tag. When `trace_votes` is on, each is
249
+ wrapped so its answer is announced as a `VoteEvent`.
250
+
251
+ ## Extending
252
+
253
+ A third-party bundle adds a kind of authenticator, token handler or user provider by prepending
254
+ its factory onto the security config from its `prepend_extension` hook — the seam an OAuth2
255
+ server plugs into:
256
+
257
+ ```python
258
+ from xtr_dependency_injection import Bundle, as_bundle, required_bundle
259
+ from xtr_security.bundle import add_authenticator_factory, add_token_handler_factory
260
+ from xtr_security.bundle import SecurityBundle
261
+
262
+
263
+ @required_bundle(SecurityBundle)
264
+ @as_bundle("oauth2")
265
+ class OAuth2Bundle(Bundle):
266
+ def prepend_extension(self, builder) -> None:
267
+ builder.prepend_extension_config("security", add_authenticator_factory(OAuth2Factory()))
268
+ builder.prepend_extension_config(
269
+ "security", add_token_handler_factory(IntrospectionFactory())
270
+ )
271
+ ```
272
+
273
+ An `AuthenticatorFactoryInterface` carries a `key`, a `priority`, a `config_type` and
274
+ `create_authenticator(...)`; a firewall lists an instance of the factory's `config_type` under
275
+ its `authenticators`, and the bundle matches it to the factory. `TokenHandlerFactoryInterface`
276
+ and `UserProviderFactoryInterface` are the same shape for the other two registries.
277
+
278
+ ## Use in an application
279
+
280
+ Everything adding this package to an application on
281
+ [xtr-dependency-injection](../xtr-dependency-injection) takes — and, read backwards, what
282
+ removing it undoes.
283
+
284
+ - **Install** — `uv add xtr-security`; `console` adds `debug:firewall` and
285
+ `security:hash-password`.
286
+ - **Recipe** — `uv run xtr-recipes recipes:sync` does the *Activate* step below: it lists
287
+ `SecurityBundle`, which brings the event dispatcher and http-kernel bundles with it. There is no
288
+ config file, environment or ignore line to write; it prints the `concurrent_scoped_access=True`,
289
+ `setup(app, kernel)` and `config/security.py` steps, which a recipe cannot make for you.
290
+ - **Activate** — `SecurityBundle: {"all": True}` in `BUNDLES` in `<app>/bundles.py`, imported
291
+ from `xtr_security.bundle`. Then call `setup(app, kernel)` where the application is built, and
292
+ build the kernel with `concurrent_scoped_access=True` so concurrent requests keep their own
293
+ token storage.
294
+ - **Brings along** — the [event dispatcher](../xtr-event-dispatcher) and
295
+ [http-kernel](../xtr-http-kernel) bundles always, because the firewalls dispatch through the
296
+ one and answer on the other's exception event; the [logging](../xtr-logging) and
297
+ [console](../xtr-console) bundles whenever those packages are installed.
298
+ - **Configure** — the zero-config path gives no firewalls and the affirmative strategy. A
299
+ `<app>/config/security.py` `@configure` function returning a `SecurityConfig` changes that —
300
+ see [Configure](#configure) and [Kernel / bundle](#kernel--bundle).
301
+ - **Environment** — nothing; an application reads its own secrets through `env(...)` in its
302
+ configuration.
303
+ - **Ignore** — nothing.
304
+ - **Remove** — drop the `BUNDLES` entry, delete `<app>/config/security.py`, then
305
+ `uv remove xtr-security`.
306
+ - **Check** — `debug:bundles` shows `security` as `listed` and `active`, `event_dispatcher` and
307
+ `http_kernel` as `required`; `debug:firewall` lists the configured firewalls.
308
+
309
+ ## Kernel / bundle
310
+
311
+ ```python
312
+ # app/bundles.py
313
+ from xtr_security.bundle import SecurityBundle
314
+
315
+ BUNDLES = {SecurityBundle: {"all": True}}
316
+ ```
317
+
318
+ `SecurityBundle` registers the token storage (scoped), the trust resolver, the role hierarchy,
319
+ the voters and the decision manager, the authorization checker and the `Security` facade (both
320
+ scoped), the password hasher factory and the user hasher, the user providers, and — per
321
+ firewall — its own event dispatcher with the login listeners, its authenticators, its access
322
+ map, its entry point and its OpenAPI scheme, gathered into a `FirewallMap`. The exception
323
+ listener turns a security error into a response on the http-kernel lifecycle. Its zero-config
324
+ path builds and boots with no configuration and touches no I/O until a request arrives.
325
+
326
+ Each firewall dispatches its security events — `CheckPassportEvent`,
327
+ `AuthenticationTokenCreatedEvent`, `AuthenticationSuccessEvent`, `LoginSuccessEvent`,
328
+ `LoginFailureEvent` — on a dispatcher of its own, named by `firewall_dispatcher_name(firewall)`.
329
+ A listener of those events on the main dispatcher hears every firewall
330
+ (`RegisterGlobalSecurityEventListenersPass` copies it onto each); one naming a firewall's
331
+ dispatcher hears that firewall alone:
332
+
333
+ ```python
334
+ from xtr_event_dispatcher import as_event_listener
335
+ from xtr_security.bundle import firewall_dispatcher_name
336
+
337
+
338
+ @as_event_listener() # every firewall
339
+ def audit_login(event: LoginSuccessEvent) -> None: ...
340
+
341
+
342
+ @as_event_listener(dispatcher=firewall_dispatcher_name("api")) # the api firewall alone
343
+ def count_api_login(event: LoginSuccessEvent) -> None: ...
344
+ ```
345
+
346
+ In debug mode `MakeFirewallsEventDispatcherTraceablePass` traces every firewall's dispatcher,
347
+ as the event dispatcher bundle traces its own; `debug:event-dispatcher --dispatcher
348
+ security.event_dispatcher.api` lists one firewall's listeners.
349
+
350
+ With a console bundle active it also registers `debug:firewall [name]`, which lists the
351
+ configured firewalls or describes one, and `security:hash-password`, which hashes a password
352
+ with the configured factory.
353
+
354
+ ## Errors
355
+
356
+ Everything the family raises derives from `SecurityError` (from
357
+ [xtr-security-core](../xtr-security-core)); this package adds one.
358
+
359
+ | Error | Raised when |
360
+ |---|---|
361
+ | `InvalidConfigurationError` | A security configuration the bundle cannot turn into services — an unknown provider, a firewall that is not stateless, an authenticator with no factory, an ambiguous entry point; also a `ValueError` |
362
+
363
+ ## Layout
364
+
365
+ ```
366
+ xtr_security/
367
+ ├── security.py the Security facade
368
+ ├── firewall_config.py FirewallConfig, the firewall an application writes
369
+ ├── firewall_context.py FirewallContext, one firewall's runtime pieces
370
+ ├── firewall_map.py FirewallMap, the container-backed map + get_firewall_config
371
+ ├── exception/ InvalidConfigurationError
372
+ ├── command/ debug:firewall
373
+ ├── factory/ authenticator factory interface + AccessTokenFactory,
374
+ │ with add_authenticator_factory
375
+ ├── access_token/ token-handler factory interface + built-ins,
376
+ │ with add_token_handler_factory
377
+ ├── user_provider/ user-provider factory interface + built-ins,
378
+ │ with add_user_provider_factory
379
+ └── bundle/
380
+ ├── security_bundle.py SecurityBundle
381
+ ├── security_config.py SecurityConfig
382
+ └── *_configs.py the configs an application writes, incl. password_hasher_configs
383
+ ```
384
+
385
+ ## Development
386
+
387
+ Developed in the [python-xtr](https://github.com/xterr/python-xtr) monorepo, under
388
+ `packages/xtr-security`; run the commands below from there. The `python-xtr-security` repository
389
+ is a read-only copy, so send issues and pull requests to the monorepo.
390
+
391
+ ```sh
392
+ uv sync --all-packages --all-extras
393
+ uv run ruff check && uv run ruff format --check && uv run basedpyright && uv run ty check && uv run pytest --cov
394
+ ```
395
+
396
+ ## License
397
+
398
+ MIT — see [LICENSE](LICENSE).