simple-module-auth 0.0.14__tar.gz → 0.0.16__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 (22) hide show
  1. {simple_module_auth-0.0.14 → simple_module_auth-0.0.16}/PKG-INFO +3 -3
  2. simple_module_auth-0.0.16/auth/__init__.py +6 -0
  3. simple_module_auth-0.0.16/auth/contracts/resolver.py +41 -0
  4. simple_module_auth-0.0.16/auth/module.py +39 -0
  5. simple_module_auth-0.0.16/auth/state.py +24 -0
  6. {simple_module_auth-0.0.14 → simple_module_auth-0.0.16}/pyproject.toml +3 -3
  7. simple_module_auth-0.0.16/tests/test_resolver_registry.py +81 -0
  8. simple_module_auth-0.0.14/auth/__init__.py +0 -1
  9. simple_module_auth-0.0.14/auth/module.py +0 -26
  10. {simple_module_auth-0.0.14 → simple_module_auth-0.0.16}/.gitignore +0 -0
  11. {simple_module_auth-0.0.14 → simple_module_auth-0.0.16}/LICENSE +0 -0
  12. {simple_module_auth-0.0.14 → simple_module_auth-0.0.16}/README.md +0 -0
  13. {simple_module_auth-0.0.14 → simple_module_auth-0.0.16}/auth/contracts/__init__.py +0 -0
  14. {simple_module_auth-0.0.14 → simple_module_auth-0.0.16}/auth/contracts/schemas.py +0 -0
  15. {simple_module_auth-0.0.14 → simple_module_auth-0.0.16}/auth/deps.py +0 -0
  16. {simple_module_auth-0.0.14 → simple_module_auth-0.0.16}/auth/locales/en.json +0 -0
  17. {simple_module_auth-0.0.14 → simple_module_auth-0.0.16}/auth/locales/es.json +0 -0
  18. {simple_module_auth-0.0.14 → simple_module_auth-0.0.16}/auth/py.typed +0 -0
  19. {simple_module_auth-0.0.14 → simple_module_auth-0.0.16}/package.json +0 -0
  20. {simple_module_auth-0.0.14 → simple_module_auth-0.0.16}/tests/test_deps.py +0 -0
  21. {simple_module_auth-0.0.14 → simple_module_auth-0.0.16}/tests/test_module.py +0 -0
  22. {simple_module_auth-0.0.14 → simple_module_auth-0.0.16}/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.14
3
+ Version: 0.0.16
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.14
26
- Requires-Dist: simple-module-db==0.0.14
25
+ Requires-Dist: simple-module-core==0.0.16
26
+ Requires-Dist: simple-module-db==0.0.16
27
27
  Description-Content-Type: text/markdown
28
28
 
29
29
  # simple_module_auth
@@ -0,0 +1,6 @@
1
+ """Auth module — shared contracts (UserContext, PrincipalResolver, deps)."""
2
+
3
+ from auth.contracts.resolver import PrincipalResolver
4
+ from auth.contracts.schemas import UserContext
5
+
6
+ __all__ = ["PrincipalResolver", "UserContext"]
@@ -0,0 +1,41 @@
1
+ """Principal-resolver extension point — apps register additional auth sources here.
2
+
3
+ A ``PrincipalResolver`` is an async callable that inspects an incoming
4
+ ``Request`` and returns a :class:`~auth.contracts.schemas.UserContext` if it
5
+ can authenticate the caller, or ``None`` to fall through to the next resolver
6
+ in the chain.
7
+
8
+ The chain is consulted by ``users.middleware.AuthMiddleware`` *after* the
9
+ session-cookie path has been tried, in registration order, and the first
10
+ non-``None`` return wins.
11
+
12
+ Invariants every resolver MUST satisfy:
13
+
14
+ * **Async.** Resolvers are awaited.
15
+ * **Cheap fast-path bail.** Resolvers run on every request; return ``None``
16
+ immediately when the credential type isn't present (e.g., no ``Authorization``
17
+ header, no matching scheme).
18
+ * **Self-checks active/disabled state.** The middleware does NOT re-validate
19
+ the user after the resolver returns — return ``None`` for disabled,
20
+ unverified, or otherwise blocked users.
21
+ * **Never raise on bad credentials.** Return ``None`` and let the chain
22
+ continue. The middleware wraps each resolver in ``try/except`` for
23
+ defense in depth, but resolver authors should not rely on it.
24
+ * **Request-scoped — no session writes.** A PAT call must not silently
25
+ elevate to a long-lived session cookie. If the resolver needs to mint a
26
+ session, that's an entirely separate code path (the standard login flow).
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ from collections.abc import Awaitable, Callable
32
+
33
+ from starlette.requests import Request
34
+
35
+ from auth.contracts.schemas import UserContext
36
+
37
+ PrincipalResolver = Callable[[Request], Awaitable[UserContext | None]]
38
+ """Async callable: ``(Request) -> UserContext | None``. See module docstring
39
+ for the invariants resolver authors must uphold."""
40
+
41
+ __all__ = ["PrincipalResolver"]
@@ -0,0 +1,39 @@
1
+ """Auth module — shared contracts (UserContext, deps).
2
+
3
+ Intentionally minimal: this module owns the PUBLIC interface (UserContext,
4
+ PrincipalResolver, get_current_user, CurrentUser, require_permission) that
5
+ every other module imports. Keeping it stable prevents churn when auth
6
+ internals change.
7
+
8
+ All authentication logic (middleware, login, signup, OAuth) lives in the
9
+ users module. The ``principal_resolvers`` registry on ``app.state.auth`` is
10
+ the extension point downstream modules use to plug in additional credential
11
+ sources (PAT bearer tokens, API keys, etc.) — see
12
+ ``docs/framework/principal-resolvers.md`` for the worked example.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import importlib.resources
18
+ from pathlib import Path
19
+ from typing import TYPE_CHECKING
20
+
21
+ from simple_module_core.module import ModuleBase, ModuleMeta
22
+
23
+ if TYPE_CHECKING:
24
+ from fastapi import FastAPI
25
+
26
+
27
+ class AuthModule(ModuleBase):
28
+ meta = ModuleMeta(
29
+ name="Auth",
30
+ route_prefix="/auth",
31
+ )
32
+
33
+ def register_settings(self, app: FastAPI) -> None:
34
+ from auth.state import AuthState
35
+
36
+ app.state.auth = AuthState()
37
+
38
+ def locale_dirs(self) -> dict[str, Path]:
39
+ return {"auth": Path(str(importlib.resources.files(__package__) / "locales"))}
@@ -0,0 +1,24 @@
1
+ """Module-owned state attached to ``app.state.auth`` by ``AuthModule.register_settings``.
2
+
3
+ Holds the principal-resolver registry (see
4
+ ``auth.contracts.resolver.PrincipalResolver``). Apps register additional
5
+ resolvers from their ``on_startup`` hook::
6
+
7
+ app.state.auth.principal_resolvers.append(my_pat_resolver)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass, field
13
+
14
+ from auth.contracts.resolver import PrincipalResolver
15
+
16
+
17
+ @dataclass
18
+ class AuthState:
19
+ """Per-app auth registry. Initialized empty; modules append resolvers."""
20
+
21
+ principal_resolvers: list[PrincipalResolver] = field(default_factory=list)
22
+
23
+
24
+ __all__ = ["AuthState"]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "simple_module_auth"
3
- version = "0.0.14"
3
+ version = "0.0.16"
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.14",
26
- "simple_module_db==0.0.14",
25
+ "simple_module_core==0.0.16",
26
+ "simple_module_db==0.0.16",
27
27
  ]
28
28
 
29
29
  [project.entry-points.simple_module]
@@ -0,0 +1,81 @@
1
+ """Tests for the auth.contracts.resolver type + AuthState registry."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Awaitable
6
+
7
+ from auth.contracts.resolver import PrincipalResolver
8
+ from auth.contracts.schemas import UserContext
9
+ from starlette.requests import Request
10
+
11
+
12
+ def test_principal_resolver_type_accepts_async_callable():
13
+ """A typical resolver signature should satisfy the type alias at runtime.
14
+
15
+ The alias is documentation + a checkable shape — we exercise the shape
16
+ by constructing one and asserting it's a Callable that returns an
17
+ awaitable.
18
+ """
19
+
20
+ async def fake_resolver(request: Request) -> UserContext | None:
21
+ return None
22
+
23
+ # Runtime — alias resolves to Callable[..., Awaitable[...]]
24
+ resolver: PrincipalResolver = fake_resolver
25
+ assert callable(resolver)
26
+ # Sanity: the function actually returns an awaitable when called.
27
+ from unittest.mock import MagicMock
28
+
29
+ result = resolver(MagicMock(spec=Request))
30
+ assert isinstance(result, Awaitable)
31
+ result.close() # don't leave an unawaited coroutine
32
+
33
+
34
+ def test_auth_state_initializes_with_empty_resolvers():
35
+ from auth.state import AuthState
36
+
37
+ state = AuthState()
38
+ assert state.principal_resolvers == []
39
+
40
+
41
+ def test_auth_state_resolvers_is_mutable_list():
42
+ """Modules register resolvers by appending; the list must be a list, not a tuple."""
43
+ from auth.state import AuthState
44
+
45
+ state = AuthState()
46
+
47
+ async def resolver(request): # pragma: no cover - registration smoke only
48
+ return None
49
+
50
+ state.principal_resolvers.append(resolver)
51
+ assert state.principal_resolvers == [resolver]
52
+
53
+
54
+ def test_auth_module_register_settings_populates_app_state():
55
+ """``AuthModule.register_settings(app)`` must put an AuthState on ``app.state.auth``."""
56
+ from auth.module import AuthModule
57
+ from auth.state import AuthState
58
+ from fastapi import FastAPI
59
+
60
+ app = FastAPI()
61
+ AuthModule().register_settings(app)
62
+
63
+ assert isinstance(app.state.auth, AuthState)
64
+ assert app.state.auth.principal_resolvers == []
65
+
66
+
67
+ def test_auth_package_reexports_public_surface():
68
+ """Downstream authors should be able to ``from auth import PrincipalResolver, UserContext``."""
69
+ import auth
70
+
71
+ assert hasattr(auth, "PrincipalResolver")
72
+ assert hasattr(auth, "UserContext")
73
+ assert "PrincipalResolver" in auth.__all__
74
+ assert "UserContext" in auth.__all__
75
+
76
+ # Identity check — re-exports point at the canonical definitions.
77
+ from auth.contracts.resolver import PrincipalResolver
78
+ from auth.contracts.schemas import UserContext
79
+
80
+ assert auth.PrincipalResolver is PrincipalResolver
81
+ assert auth.UserContext is UserContext
@@ -1 +0,0 @@
1
- """Auth module — shared contracts (UserContext, deps)."""
@@ -1,26 +0,0 @@
1
- """Auth module — shared contracts (UserContext, deps).
2
-
3
- Intentionally minimal: this module owns the PUBLIC interface (UserContext,
4
- get_current_user, CurrentUser, require_permission) that every other module
5
- imports. Keeping it stable prevents churn when auth internals change.
6
-
7
- All authentication logic (middleware, login, signup, OAuth) lives in the
8
- users module.
9
- """
10
-
11
- from __future__ import annotations
12
-
13
- import importlib.resources
14
- from pathlib import Path
15
-
16
- from simple_module_core.module import ModuleBase, ModuleMeta
17
-
18
-
19
- class AuthModule(ModuleBase):
20
- meta = ModuleMeta(
21
- name="Auth",
22
- route_prefix="/auth",
23
- )
24
-
25
- def locale_dirs(self) -> dict[str, Path]:
26
- return {"auth": Path(str(importlib.resources.files(__package__) / "locales"))}