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.
- {simple_module_auth-0.0.14 → simple_module_auth-0.0.16}/PKG-INFO +3 -3
- simple_module_auth-0.0.16/auth/__init__.py +6 -0
- simple_module_auth-0.0.16/auth/contracts/resolver.py +41 -0
- simple_module_auth-0.0.16/auth/module.py +39 -0
- simple_module_auth-0.0.16/auth/state.py +24 -0
- {simple_module_auth-0.0.14 → simple_module_auth-0.0.16}/pyproject.toml +3 -3
- simple_module_auth-0.0.16/tests/test_resolver_registry.py +81 -0
- simple_module_auth-0.0.14/auth/__init__.py +0 -1
- simple_module_auth-0.0.14/auth/module.py +0 -26
- {simple_module_auth-0.0.14 → simple_module_auth-0.0.16}/.gitignore +0 -0
- {simple_module_auth-0.0.14 → simple_module_auth-0.0.16}/LICENSE +0 -0
- {simple_module_auth-0.0.14 → simple_module_auth-0.0.16}/README.md +0 -0
- {simple_module_auth-0.0.14 → simple_module_auth-0.0.16}/auth/contracts/__init__.py +0 -0
- {simple_module_auth-0.0.14 → simple_module_auth-0.0.16}/auth/contracts/schemas.py +0 -0
- {simple_module_auth-0.0.14 → simple_module_auth-0.0.16}/auth/deps.py +0 -0
- {simple_module_auth-0.0.14 → simple_module_auth-0.0.16}/auth/locales/en.json +0 -0
- {simple_module_auth-0.0.14 → simple_module_auth-0.0.16}/auth/locales/es.json +0 -0
- {simple_module_auth-0.0.14 → simple_module_auth-0.0.16}/auth/py.typed +0 -0
- {simple_module_auth-0.0.14 → simple_module_auth-0.0.16}/package.json +0 -0
- {simple_module_auth-0.0.14 → simple_module_auth-0.0.16}/tests/test_deps.py +0 -0
- {simple_module_auth-0.0.14 → simple_module_auth-0.0.16}/tests/test_module.py +0 -0
- {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.
|
|
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.
|
|
26
|
-
Requires-Dist: simple-module-db==0.0.
|
|
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,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.
|
|
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.
|
|
26
|
-
"simple_module_db==0.0.
|
|
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"))}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|