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.
- xtr_security-3.0.0/LICENSE +21 -0
- xtr_security-3.0.0/PKG-INFO +398 -0
- xtr_security-3.0.0/README.md +370 -0
- xtr_security-3.0.0/pyproject.toml +165 -0
- xtr_security-3.0.0/pyproject.toml.orig +145 -0
- xtr_security-3.0.0/src/xtr_security/.agents/skills/xtr-security/SKILL.md +291 -0
- xtr_security-3.0.0/src/xtr_security/.agents/skills/xtr-security/references/authorization.md +149 -0
- xtr_security-3.0.0/src/xtr_security/.agents/skills/xtr-security/references/firewalls-and-authenticators.md +165 -0
- xtr_security-3.0.0/src/xtr_security/.agents/skills/xtr-security/references/password-hashing.md +122 -0
- xtr_security-3.0.0/src/xtr_security/__init__.py +13 -0
- xtr_security-3.0.0/src/xtr_security/access_token/__init__.py +19 -0
- xtr_security-3.0.0/src/xtr_security/access_token/add_token_handler_factory.py +35 -0
- xtr_security-3.0.0/src/xtr_security/access_token/oidc_token_handler_factory.py +114 -0
- xtr_security-3.0.0/src/xtr_security/access_token/service_token_handler_factory.py +48 -0
- xtr_security-3.0.0/src/xtr_security/access_token/token_handler_factory_interface.py +52 -0
- xtr_security-3.0.0/src/xtr_security/bundle/__init__.py +97 -0
- xtr_security-3.0.0/src/xtr_security/bundle/_firewall_scheme_middleware.py +38 -0
- xtr_security-3.0.0/src/xtr_security/bundle/_firewall_scheme_middleware_factory.py +33 -0
- xtr_security-3.0.0/src/xtr_security/bundle/_password_hasher_build.py +83 -0
- xtr_security-3.0.0/src/xtr_security/bundle/_wiring.py +736 -0
- xtr_security-3.0.0/src/xtr_security/bundle/access_control_configs.py +83 -0
- xtr_security-3.0.0/src/xtr_security/bundle/access_decision_manager_config.py +66 -0
- xtr_security-3.0.0/src/xtr_security/bundle/authenticator_configs.py +56 -0
- xtr_security-3.0.0/src/xtr_security/bundle/firewall_dispatcher_name.py +19 -0
- xtr_security-3.0.0/src/xtr_security/bundle/make_firewalls_event_dispatcher_traceable_pass.py +53 -0
- xtr_security-3.0.0/src/xtr_security/bundle/password_hasher_configs.py +166 -0
- xtr_security-3.0.0/src/xtr_security/bundle/register_global_security_event_listeners_pass.py +69 -0
- xtr_security-3.0.0/src/xtr_security/bundle/security_bundle.py +305 -0
- xtr_security-3.0.0/src/xtr_security/bundle/security_config.py +155 -0
- xtr_security-3.0.0/src/xtr_security/bundle/token_handler_configs.py +120 -0
- xtr_security-3.0.0/src/xtr_security/bundle/user_provider_configs.py +98 -0
- xtr_security-3.0.0/src/xtr_security/command/__init__.py +15 -0
- xtr_security-3.0.0/src/xtr_security/command/debug_firewall_command.py +76 -0
- xtr_security-3.0.0/src/xtr_security/exception/__init__.py +12 -0
- xtr_security-3.0.0/src/xtr_security/exception/invalid_configuration_error.py +20 -0
- xtr_security-3.0.0/src/xtr_security/factory/__init__.py +17 -0
- xtr_security-3.0.0/src/xtr_security/factory/access_token_factory.py +220 -0
- xtr_security-3.0.0/src/xtr_security/factory/add_authenticator_factory.py +36 -0
- xtr_security-3.0.0/src/xtr_security/factory/authenticator_factory_interface.py +68 -0
- xtr_security-3.0.0/src/xtr_security/firewall_config.py +107 -0
- xtr_security-3.0.0/src/xtr_security/firewall_context.py +68 -0
- xtr_security-3.0.0/src/xtr_security/firewall_map.py +93 -0
- xtr_security-3.0.0/src/xtr_security/py.typed +0 -0
- xtr_security-3.0.0/src/xtr_security/recipe/__init__.py +16 -0
- xtr_security-3.0.0/src/xtr_security/recipe/manifest.toml +30 -0
- xtr_security-3.0.0/src/xtr_security/security.py +108 -0
- xtr_security-3.0.0/src/xtr_security/user_provider/__init__.py +21 -0
- xtr_security-3.0.0/src/xtr_security/user_provider/add_user_provider_factory.py +35 -0
- xtr_security-3.0.0/src/xtr_security/user_provider/chain_factory.py +82 -0
- xtr_security-3.0.0/src/xtr_security/user_provider/in_memory_factory.py +68 -0
- xtr_security-3.0.0/src/xtr_security/user_provider/service_factory.py +48 -0
- 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).
|