simple-module-auth 0.0.17__tar.gz → 0.0.18__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 (24) hide show
  1. {simple_module_auth-0.0.17 → simple_module_auth-0.0.18}/PKG-INFO +3 -3
  2. {simple_module_auth-0.0.17 → simple_module_auth-0.0.18}/auth/middleware.py +11 -1
  3. {simple_module_auth-0.0.17 → simple_module_auth-0.0.18}/pyproject.toml +3 -3
  4. {simple_module_auth-0.0.17 → simple_module_auth-0.0.18}/tests/test_auth_middleware.py +47 -3
  5. {simple_module_auth-0.0.17 → simple_module_auth-0.0.18}/.gitignore +0 -0
  6. {simple_module_auth-0.0.17 → simple_module_auth-0.0.18}/LICENSE +0 -0
  7. {simple_module_auth-0.0.17 → simple_module_auth-0.0.18}/README.md +0 -0
  8. {simple_module_auth-0.0.17 → simple_module_auth-0.0.18}/auth/__init__.py +0 -0
  9. {simple_module_auth-0.0.17 → simple_module_auth-0.0.18}/auth/contracts/__init__.py +0 -0
  10. {simple_module_auth-0.0.17 → simple_module_auth-0.0.18}/auth/contracts/provider.py +0 -0
  11. {simple_module_auth-0.0.17 → simple_module_auth-0.0.18}/auth/contracts/resolver.py +0 -0
  12. {simple_module_auth-0.0.17 → simple_module_auth-0.0.18}/auth/contracts/schemas.py +0 -0
  13. {simple_module_auth-0.0.17 → simple_module_auth-0.0.18}/auth/deps.py +0 -0
  14. {simple_module_auth-0.0.17 → simple_module_auth-0.0.18}/auth/locales/en.json +0 -0
  15. {simple_module_auth-0.0.17 → simple_module_auth-0.0.18}/auth/locales/es.json +0 -0
  16. {simple_module_auth-0.0.17 → simple_module_auth-0.0.18}/auth/module.py +0 -0
  17. {simple_module_auth-0.0.17 → simple_module_auth-0.0.18}/auth/py.typed +0 -0
  18. {simple_module_auth-0.0.17 → simple_module_auth-0.0.18}/auth/state.py +0 -0
  19. {simple_module_auth-0.0.17 → simple_module_auth-0.0.18}/package.json +0 -0
  20. {simple_module_auth-0.0.17 → simple_module_auth-0.0.18}/tests/test_auth_provider_protocol.py +0 -0
  21. {simple_module_auth-0.0.17 → simple_module_auth-0.0.18}/tests/test_deps.py +0 -0
  22. {simple_module_auth-0.0.17 → simple_module_auth-0.0.18}/tests/test_module.py +0 -0
  23. {simple_module_auth-0.0.17 → simple_module_auth-0.0.18}/tests/test_resolver_registry.py +0 -0
  24. {simple_module_auth-0.0.17 → simple_module_auth-0.0.18}/tests/test_user_context.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simple_module_auth
3
- Version: 0.0.17
3
+ Version: 0.0.18
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
@@ -22,8 +22,8 @@ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
22
22
  Classifier: Typing :: Typed
23
23
  Requires-Python: >=3.12
24
24
  Requires-Dist: itsdangerous>=2.2
25
- Requires-Dist: simple-module-core==0.0.17
26
- Requires-Dist: simple-module-db==0.0.17
25
+ Requires-Dist: simple-module-core==0.0.18
26
+ Requires-Dist: simple-module-db==0.0.18
27
27
  Description-Content-Type: text/markdown
28
28
 
29
29
  # simple_module_auth
@@ -47,7 +47,9 @@ class AuthMiddleware:
47
47
  return
48
48
 
49
49
  path: str = scope["path"]
50
- auth_state = scope["app"].state.auth
50
+ method: str = scope.get("method", "GET")
51
+ app_state = scope["app"].state
52
+ auth_state = app_state.auth
51
53
  provider = auth_state.auth_provider
52
54
 
53
55
  if provider is None:
@@ -58,6 +60,14 @@ class AuthMiddleware:
58
60
  any(path.startswith(p) for p in _FRAMEWORK_PUBLIC_PREFIXES)
59
61
  or path in _FRAMEWORK_PUBLIC_EXACT
60
62
  )
63
+ # Module-contributed public routes (register_public_routes hook). Method
64
+ # -aware, so a GET read route can be exempted without opening sibling
65
+ # POST/PATCH mutations under the same prefix.
66
+ if not is_public:
67
+ public_routes = getattr(app_state, "public_routes", None)
68
+ is_public = public_routes is not None and public_routes.matches(method, path)
69
+ # Legacy provider-declared paths (prefix-only, method-agnostic). Kept for
70
+ # back-compat with AuthProvider implementations.
61
71
  if not is_public:
62
72
  prefix_paths, exact_paths = provider.get_public_paths()
63
73
  is_public = any(path.startswith(p) for p in prefix_paths) or path in exact_paths
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "simple_module_auth"
3
- version = "0.0.17"
3
+ version = "0.0.18"
4
4
  description = "Session-cookie authentication primitives — middleware, login/logout, redirect helpers for simple_module"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -22,8 +22,8 @@ classifiers = [
22
22
  ]
23
23
  dependencies = [
24
24
  "itsdangerous>=2.2",
25
- "simple_module_core==0.0.17",
26
- "simple_module_db==0.0.17",
25
+ "simple_module_core==0.0.18",
26
+ "simple_module_db==0.0.18",
27
27
  ]
28
28
 
29
29
  [project.entry-points.simple_module]
@@ -44,15 +44,16 @@ class _StubProvider:
44
44
  return auth.startswith("Bearer ")
45
45
 
46
46
 
47
- def _build_app(provider, *, principal_resolvers=None):
47
+ def _build_app(provider, *, principal_resolvers=None, public_routes=None):
48
48
  app = FastAPI()
49
49
  app.state.auth = AuthState(
50
50
  auth_provider=provider,
51
51
  principal_resolvers=list(principal_resolvers or []),
52
52
  )
53
+ if public_routes is not None:
54
+ app.state.public_routes = public_routes
53
55
 
54
- @app.get("/{path:path}")
55
- async def catch_all(request: Request, path: str = ""):
56
+ async def _handler(request: Request, path: str = ""):
56
57
  user = getattr(request.state, "user", None)
57
58
  return JSONResponse(
58
59
  {
@@ -60,6 +61,8 @@ def _build_app(provider, *, principal_resolvers=None):
60
61
  }
61
62
  )
62
63
 
64
+ app.add_api_route("/{path:path}", _handler, methods=["GET", "POST", "PATCH"])
65
+
63
66
  app.add_middleware(AuthMiddleware)
64
67
  app.add_middleware(SessionMiddleware, secret_key=SECRET)
65
68
  return app
@@ -129,6 +132,47 @@ async def test_root_is_public(unauthenticated_app):
129
132
  assert resp.status_code == 200
130
133
 
131
134
 
135
+ async def test_registry_public_route_skips_auth():
136
+ """A module-contributed public route lets an unauthenticated GET through."""
137
+ from simple_module_core.public_routes import PublicRouteRegistry
138
+
139
+ registry = PublicRouteRegistry()
140
+ registry.add_prefix("/api/gis/stac")
141
+ app = _build_app(_StubProvider(user=None), public_routes=registry)
142
+
143
+ transport = httpx.ASGITransport(app=app)
144
+ async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
145
+ resp = await c.get("/api/gis/stac/collections")
146
+ assert resp.status_code == 200
147
+ assert resp.json()["user"] is None
148
+
149
+
150
+ async def test_registry_method_scoping_gates_other_verbs():
151
+ """A GET-scoped public rule exempts GET but still gates PATCH on the same path."""
152
+ from simple_module_core.public_routes import PublicRouteRegistry
153
+
154
+ registry = PublicRouteRegistry()
155
+ registry.add_regex(r"/api/gis/datasets/[^/]+/tilejson$", methods={"GET"})
156
+ app = _build_app(_StubProvider(user=None), public_routes=registry)
157
+
158
+ transport = httpx.ASGITransport(app=app)
159
+ async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
160
+ ok = await c.get("/api/gis/datasets/42/tilejson")
161
+ gated = await c.patch("/api/gis/datasets/42/tilejson")
162
+ assert ok.status_code == 200
163
+ assert gated.status_code == 401
164
+
165
+
166
+ async def test_no_registry_falls_back_to_provider_paths(unauthenticated_app):
167
+ """Apps built without a public-routes registry still honor provider paths."""
168
+ transport = httpx.ASGITransport(app=unauthenticated_app)
169
+ async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
170
+ public = await c.get("/stub/public/data")
171
+ gated = await c.get("/api/protected")
172
+ assert public.status_code == 200
173
+ assert gated.status_code == 401
174
+
175
+
132
176
  async def test_resolver_chain_fallback():
133
177
  """When provider returns None, fall through to principal resolvers."""
134
178