simple-module-auth 0.0.37__tar.gz → 0.0.39__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 (30) hide show
  1. {simple_module_auth-0.0.37 → simple_module_auth-0.0.39}/.gitignore +1 -2
  2. {simple_module_auth-0.0.37 → simple_module_auth-0.0.39}/PKG-INFO +6 -5
  3. {simple_module_auth-0.0.37 → simple_module_auth-0.0.39}/README.md +1 -1
  4. {simple_module_auth-0.0.37 → simple_module_auth-0.0.39}/auth/contracts/provider.py +0 -2
  5. {simple_module_auth-0.0.37 → simple_module_auth-0.0.39}/auth/contracts/schemas.py +20 -7
  6. simple_module_auth-0.0.39/auth/locales/de.json +6 -0
  7. {simple_module_auth-0.0.37 → simple_module_auth-0.0.39}/auth/middleware.py +9 -11
  8. {simple_module_auth-0.0.37 → simple_module_auth-0.0.39}/auth/state.py +9 -0
  9. {simple_module_auth-0.0.37 → simple_module_auth-0.0.39}/package.json +1 -1
  10. {simple_module_auth-0.0.37 → simple_module_auth-0.0.39}/pyproject.toml +6 -4
  11. {simple_module_auth-0.0.37 → simple_module_auth-0.0.39}/LICENSE +0 -0
  12. {simple_module_auth-0.0.37 → simple_module_auth-0.0.39}/auth/__init__.py +0 -0
  13. {simple_module_auth-0.0.37 → simple_module_auth-0.0.39}/auth/_route_match.py +0 -0
  14. {simple_module_auth-0.0.37 → simple_module_auth-0.0.39}/auth/contracts/__init__.py +0 -0
  15. {simple_module_auth-0.0.37 → simple_module_auth-0.0.39}/auth/contracts/resolver.py +0 -0
  16. {simple_module_auth-0.0.37 → simple_module_auth-0.0.39}/auth/contracts/tokens.py +0 -0
  17. {simple_module_auth-0.0.37 → simple_module_auth-0.0.39}/auth/deps.py +0 -0
  18. {simple_module_auth-0.0.37 → simple_module_auth-0.0.39}/auth/locales/en.json +0 -0
  19. {simple_module_auth-0.0.37 → simple_module_auth-0.0.39}/auth/locales/es.json +0 -0
  20. {simple_module_auth-0.0.37 → simple_module_auth-0.0.39}/auth/module.py +0 -0
  21. {simple_module_auth-0.0.37 → simple_module_auth-0.0.39}/auth/py.typed +0 -0
  22. {simple_module_auth-0.0.37 → simple_module_auth-0.0.39}/tests/test_auth_middleware.py +0 -0
  23. {simple_module_auth-0.0.37 → simple_module_auth-0.0.39}/tests/test_auth_next_target.py +0 -0
  24. {simple_module_auth-0.0.37 → simple_module_auth-0.0.39}/tests/test_auth_parse_bearer.py +0 -0
  25. {simple_module_auth-0.0.37 → simple_module_auth-0.0.39}/tests/test_auth_provider_protocol.py +0 -0
  26. {simple_module_auth-0.0.37 → simple_module_auth-0.0.39}/tests/test_auth_unknown_paths.py +0 -0
  27. {simple_module_auth-0.0.37 → simple_module_auth-0.0.39}/tests/test_deps.py +0 -0
  28. {simple_module_auth-0.0.37 → simple_module_auth-0.0.39}/tests/test_module.py +0 -0
  29. {simple_module_auth-0.0.37 → simple_module_auth-0.0.39}/tests/test_resolver_registry.py +0 -0
  30. {simple_module_auth-0.0.37 → simple_module_auth-0.0.39}/tests/test_user_context.py +0 -0
@@ -7,8 +7,6 @@ build/
7
7
  .venv/
8
8
  *.egg
9
9
 
10
- # UV
11
- uv.lock
12
10
 
13
11
  # Node
14
12
  node_modules/
@@ -65,3 +63,4 @@ host/client_app/.playwright-cli/*
65
63
  .qa/
66
64
  .verify/
67
65
  /openapi.json
66
+ qa-shots/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: simple_module_auth
3
- Version: 0.0.37
3
+ Version: 0.0.39
4
4
  Summary: Session-cookie authentication primitives — middleware, login/logout, redirect helpers for simple_module
5
5
  Project-URL: Homepage, https://github.com/antosubash/simple_module_python
6
6
  Project-URL: Repository, https://github.com/antosubash/simple_module_python
@@ -21,9 +21,10 @@ Classifier: Topic :: Internet :: WWW/HTTP
21
21
  Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
22
22
  Classifier: Typing :: Typed
23
23
  Requires-Python: >=3.12
24
- Requires-Dist: itsdangerous>=2.2
25
- Requires-Dist: simple-module-core==0.0.37
26
- Requires-Dist: simple-module-db==0.0.37
24
+ Requires-Dist: itsdangerous<3,>=2.2
25
+ Requires-Dist: simple-module-core==0.0.39
26
+ Requires-Dist: simple-module-db==0.0.39
27
+ Requires-Dist: simple-module-hosting==0.0.39
27
28
  Description-Content-Type: text/markdown
28
29
 
29
30
  # simple_module_auth
@@ -41,7 +42,7 @@ pip install simple_module_auth
41
42
  ## What it provides
42
43
 
43
44
  - `UserContext` — the request-scoped principal (`id`, `name`, `email`, `roles`).
44
- - `AuthProvider` protocol — the swappable-backend contract (`resolve_user`, `get_login_url`, `get_logout_url`, `get_public_paths`, `is_bearer_request`); exactly one provider module registers an implementation on `app.state.auth.auth_provider`.
45
+ - `AuthProvider` protocol — the swappable-backend contract (`resolve_user`, `get_login_url`, `get_logout_url`, `is_bearer_request`); exactly one provider module registers an implementation on `app.state.auth.auth_provider`.
45
46
  - `PrincipalResolver` chain — async `(Request) -> UserContext | None` callables apps append to `app.state.auth.principal_resolvers` (e.g. PAT/bearer-token or API-key auth), consulted after the session path.
46
47
  - `AuthMiddleware` — delegates to the provider + resolver chain on every request and populates the request principal.
47
48
  - `get_current_user` / `CurrentUser` dependency and the `require_permission(*permissions)` dependency factory.
@@ -13,7 +13,7 @@ pip install simple_module_auth
13
13
  ## What it provides
14
14
 
15
15
  - `UserContext` — the request-scoped principal (`id`, `name`, `email`, `roles`).
16
- - `AuthProvider` protocol — the swappable-backend contract (`resolve_user`, `get_login_url`, `get_logout_url`, `get_public_paths`, `is_bearer_request`); exactly one provider module registers an implementation on `app.state.auth.auth_provider`.
16
+ - `AuthProvider` protocol — the swappable-backend contract (`resolve_user`, `get_login_url`, `get_logout_url`, `is_bearer_request`); exactly one provider module registers an implementation on `app.state.auth.auth_provider`.
17
17
  - `PrincipalResolver` chain — async `(Request) -> UserContext | None` callables apps append to `app.state.auth.principal_resolvers` (e.g. PAT/bearer-token or API-key auth), consulted after the session path.
18
18
  - `AuthMiddleware` — delegates to the provider + resolver chain on every request and populates the request principal.
19
19
  - `get_current_user` / `CurrentUser` dependency and the `require_permission(*permissions)` dependency factory.
@@ -26,8 +26,6 @@ class AuthProvider(Protocol):
26
26
 
27
27
  def get_logout_url(self, request: Request) -> str: ...
28
28
 
29
- def get_public_paths(self) -> tuple[tuple[str, ...], tuple[str, ...]]: ...
30
-
31
29
  def is_bearer_request(self, request: Request) -> bool: ...
32
30
 
33
31
 
@@ -2,15 +2,28 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from collections.abc import Iterable
5
6
  from dataclasses import dataclass, field
6
- from typing import TYPE_CHECKING, Any
7
+ from typing import Any, Protocol
7
8
 
8
9
  from auth.contracts.tokens import AUTH_KIND_SESSION
9
10
 
10
- if TYPE_CHECKING:
11
- # Runtime import would be circular: auth -> users -> auth.
12
- # Only imported for type-hints, never at runtime.
13
- from users.models import User
11
+
12
+ class NamedRole(Protocol):
13
+ name: str
14
+
15
+
16
+ class AccountLike(Protocol):
17
+ """What :meth:`UserContext.from_user` reads from an account row.
18
+
19
+ Structural, so ``auth`` names no provider's model: ``users`` depends on
20
+ ``auth``, never the other way round (GH #501).
21
+ """
22
+
23
+ id: Any
24
+ email: str
25
+ full_name: str | None
26
+ roles: Iterable[NamedRole]
14
27
 
15
28
 
16
29
  @dataclass
@@ -53,12 +66,12 @@ class UserContext:
53
66
  @classmethod
54
67
  def from_user(
55
68
  cls,
56
- user: User | Any,
69
+ user: AccountLike,
57
70
  *,
58
71
  auth_kind: str = AUTH_KIND_SESSION,
59
72
  scopes: list[str] | None = None,
60
73
  ) -> UserContext:
61
- """Build a UserContext from a users.models.User with eagerly-loaded roles.
74
+ """Build a UserContext from an :class:`AccountLike` with eagerly-loaded roles.
62
75
 
63
76
  Duck-typed to avoid importing users.models at runtime — any object
64
77
  exposing .id, .email, .full_name, .roles[*].name works. The user row
@@ -0,0 +1,6 @@
1
+ {
2
+ "errors": {
3
+ "not_authenticated": "Nicht authentifiziert",
4
+ "missing_permission": "Erforderliche Berechtigung fehlt: {permissions}"
5
+ }
6
+ }
@@ -10,8 +10,10 @@ from __future__ import annotations
10
10
 
11
11
  import logging
12
12
 
13
+ from simple_module_core.app_state import get_openapi_url
13
14
  from simple_module_core.redirect_safety import SESSION_NEXT_KEY, safe_next_or_none
14
15
  from simple_module_db.listeners import current_user_id
16
+ from simple_module_hosting.public_access import is_public as route_is_public
15
17
  from starlette.requests import Request
16
18
  from starlette.responses import JSONResponse, RedirectResponse
17
19
  from starlette.types import ASGIApp, Receive, Scope, Send
@@ -86,19 +88,10 @@ class AuthMiddleware:
86
88
  any(path.startswith(p) for p in _FRAMEWORK_PUBLIC_PREFIXES)
87
89
  or path in _FRAMEWORK_PUBLIC_EXACT
88
90
  # Anonymous exactly when the host serves it (dev, or openapi_public).
89
- or path == getattr(app_state, "openapi_url", None)
91
+ or path == get_openapi_url(scope["app"])
90
92
  )
91
- # Module-contributed public routes (register_public_routes hook). Method
92
- # -aware, so a GET read route can be exempted without opening sibling
93
- # POST/PATCH mutations under the same prefix.
94
93
  if not is_public:
95
- public_routes = getattr(app_state, "public_routes", None)
96
- is_public = public_routes is not None and public_routes.matches(method, path)
97
- # Legacy provider-declared paths (prefix-only, method-agnostic). Kept for
98
- # back-compat with AuthProvider implementations.
99
- if not is_public:
100
- prefix_paths, exact_paths = provider.get_public_paths()
101
- is_public = any(path.startswith(p) for p in prefix_paths) or path in exact_paths
94
+ is_public = route_is_public(scope["app"], method, path)
102
95
 
103
96
  # Contract with the host's RateLimitMiddleware (plain scope key, no import
104
97
  # between them): whether *this* middleware treats the request as anonymous-
@@ -158,6 +151,11 @@ class AuthMiddleware:
158
151
 
159
152
  if user_ctx is not None:
160
153
  request.state.user = user_ctx
154
+ for guard in auth_state.principal_guards:
155
+ refusal = guard(request)
156
+ if refusal is not None:
157
+ await refusal(scope, receive, send)
158
+ return
161
159
  token = current_user_id.set(user_ctx.id)
162
160
  try:
163
161
  await self.app(scope, receive, send)
@@ -9,12 +9,16 @@ principal-resolver registry. Apps register additional resolvers from their
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
+ from collections.abc import Callable
12
13
  from dataclasses import dataclass, field
13
14
  from typing import TYPE_CHECKING
14
15
 
15
16
  from auth.contracts.resolver import PrincipalResolver
16
17
 
17
18
  if TYPE_CHECKING:
19
+ from starlette.requests import Request
20
+ from starlette.responses import Response
21
+
18
22
  from auth.contracts.provider import AuthProvider
19
23
 
20
24
 
@@ -24,6 +28,11 @@ class AuthState:
24
28
 
25
29
  auth_provider: AuthProvider | None = None
26
30
  principal_resolvers: list[PrincipalResolver] = field(default_factory=list)
31
+ # Run once a principal has resolved (any credential source), before downstream
32
+ # middleware; anonymous requests never reach them.
33
+ # A guard is a sync callable returning a refusal response or None to continue.
34
+ # An exception propagates (a 500), so a broken guard fails closed, never open.
35
+ principal_guards: list[Callable[[Request], Response | None]] = field(default_factory=list)
27
36
 
28
37
 
29
38
  __all__ = ["AuthState"]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simple-module-py/auth",
3
- "version": "0.1.0",
3
+ "version": "0.0.39",
4
4
  "private": true,
5
5
  "description": "Frontend assets for the Auth module",
6
6
  "peerDependencies": {
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "simple_module_auth"
3
- version = "0.0.37"
3
+ version = "0.0.39"
4
4
  description = "Session-cookie authentication primitives — middleware, login/logout, redirect helpers for simple_module"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -21,9 +21,10 @@ classifiers = [
21
21
  "Typing :: Typed",
22
22
  ]
23
23
  dependencies = [
24
- "itsdangerous>=2.2",
25
- "simple_module_core==0.0.37",
26
- "simple_module_db==0.0.37",
24
+ "itsdangerous>=2.2,<3",
25
+ "simple_module_core==0.0.39",
26
+ "simple_module_db==0.0.39",
27
+ "simple_module_hosting==0.0.39",
27
28
  ]
28
29
 
29
30
  [project.entry-points.simple_module]
@@ -50,3 +51,4 @@ packages = ["auth"]
50
51
  [tool.uv.sources]
51
52
  simple_module_core = { workspace = true }
52
53
  simple_module_db = { workspace = true }
54
+ simple_module_hosting = { workspace = true }