core-framework 0.12.4__py3-none-any.whl → 0.12.6__py3-none-any.whl
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.
- core/__init__.py +1 -1
- core/auth/__init__.py +15 -0
- core/auth/backends.py +5 -5
- core/auth/decorators.py +7 -6
- core/auth/helpers.py +104 -0
- core/auth/middleware.py +286 -168
- core/auth/tokens.py +17 -3
- core/auth/views.py +27 -7
- core/cli/main.py +6 -4
- core/dependencies.py +3 -1
- core/permissions.py +8 -6
- core/tenancy.py +3 -2
- {core_framework-0.12.4.dist-info → core_framework-0.12.6.dist-info}/METADATA +1 -1
- {core_framework-0.12.4.dist-info → core_framework-0.12.6.dist-info}/RECORD +16 -15
- {core_framework-0.12.4.dist-info → core_framework-0.12.6.dist-info}/WHEEL +0 -0
- {core_framework-0.12.4.dist-info → core_framework-0.12.6.dist-info}/entry_points.txt +0 -0
core/__init__.py
CHANGED
core/auth/__init__.py
CHANGED
|
@@ -121,9 +121,18 @@ from core.auth.views import (
|
|
|
121
121
|
from core.auth.middleware import (
|
|
122
122
|
AuthenticationMiddleware,
|
|
123
123
|
OptionalAuthenticationMiddleware,
|
|
124
|
+
JWTAuthBackend,
|
|
125
|
+
AuthenticatedUser,
|
|
124
126
|
ensure_auth_middleware,
|
|
125
127
|
)
|
|
126
128
|
|
|
129
|
+
# Helper functions for consistent user access
|
|
130
|
+
from core.auth.helpers import (
|
|
131
|
+
get_request_user,
|
|
132
|
+
is_authenticated,
|
|
133
|
+
set_request_user,
|
|
134
|
+
)
|
|
135
|
+
|
|
127
136
|
__all__ = [
|
|
128
137
|
# Base
|
|
129
138
|
"AuthBackend",
|
|
@@ -196,5 +205,11 @@ __all__ = [
|
|
|
196
205
|
# Middleware
|
|
197
206
|
"AuthenticationMiddleware",
|
|
198
207
|
"OptionalAuthenticationMiddleware",
|
|
208
|
+
"JWTAuthBackend",
|
|
209
|
+
"AuthenticatedUser",
|
|
199
210
|
"ensure_auth_middleware",
|
|
211
|
+
# Helpers
|
|
212
|
+
"get_request_user",
|
|
213
|
+
"is_authenticated",
|
|
214
|
+
"set_request_user",
|
|
200
215
|
]
|
core/auth/backends.py
CHANGED
|
@@ -161,14 +161,14 @@ class ModelBackend(AuthBackend):
|
|
|
161
161
|
|
|
162
162
|
async def login(self, request: "Request", user: Any) -> None:
|
|
163
163
|
"""Executa ações pós-login."""
|
|
164
|
-
# Armazena usuário no request.state
|
|
165
|
-
|
|
164
|
+
# Armazena usuário no request.state (for backward compatibility)
|
|
165
|
+
from core.auth.helpers import set_request_user
|
|
166
|
+
set_request_user(request, user)
|
|
166
167
|
|
|
167
168
|
async def logout(self, request: "Request", user: Any) -> None:
|
|
168
169
|
"""Executa ações de logout."""
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
request.state.user = None
|
|
170
|
+
from core.auth.helpers import set_request_user
|
|
171
|
+
set_request_user(request, None)
|
|
172
172
|
|
|
173
173
|
|
|
174
174
|
class TokenAuthBackend(AuthBackend):
|
core/auth/decorators.py
CHANGED
|
@@ -24,6 +24,7 @@ from typing import Any, TYPE_CHECKING
|
|
|
24
24
|
from fastapi import Depends, HTTPException, Request, status
|
|
25
25
|
|
|
26
26
|
from core.permissions import Permission as PermissionBase
|
|
27
|
+
from core.auth.helpers import get_request_user
|
|
27
28
|
|
|
28
29
|
if TYPE_CHECKING:
|
|
29
30
|
pass
|
|
@@ -70,7 +71,7 @@ class HasPermission(PermissionBase):
|
|
|
70
71
|
request: Request,
|
|
71
72
|
view: Any = None,
|
|
72
73
|
) -> bool:
|
|
73
|
-
user =
|
|
74
|
+
user = get_request_user(request)
|
|
74
75
|
|
|
75
76
|
if user is None:
|
|
76
77
|
return False
|
|
@@ -135,7 +136,7 @@ class IsInGroup(PermissionBase):
|
|
|
135
136
|
request: Request,
|
|
136
137
|
view: Any = None,
|
|
137
138
|
) -> bool:
|
|
138
|
-
user =
|
|
139
|
+
user = get_request_user(request)
|
|
139
140
|
|
|
140
141
|
if user is None:
|
|
141
142
|
return False
|
|
@@ -179,7 +180,7 @@ class IsSuperuser(PermissionBase):
|
|
|
179
180
|
request: Request,
|
|
180
181
|
view: Any = None,
|
|
181
182
|
) -> bool:
|
|
182
|
-
user =
|
|
183
|
+
user = get_request_user(request)
|
|
183
184
|
|
|
184
185
|
if user is None:
|
|
185
186
|
return False
|
|
@@ -207,7 +208,7 @@ class IsStaff(PermissionBase):
|
|
|
207
208
|
request: Request,
|
|
208
209
|
view: Any = None,
|
|
209
210
|
) -> bool:
|
|
210
|
-
user =
|
|
211
|
+
user = get_request_user(request)
|
|
211
212
|
|
|
212
213
|
if user is None:
|
|
213
214
|
return False
|
|
@@ -235,7 +236,7 @@ class IsActive(PermissionBase):
|
|
|
235
236
|
request: Request,
|
|
236
237
|
view: Any = None,
|
|
237
238
|
) -> bool:
|
|
238
|
-
user =
|
|
239
|
+
user = get_request_user(request)
|
|
239
240
|
|
|
240
241
|
if user is None:
|
|
241
242
|
return False
|
|
@@ -371,7 +372,7 @@ def login_required():
|
|
|
371
372
|
...
|
|
372
373
|
"""
|
|
373
374
|
async def check(request: Request):
|
|
374
|
-
user =
|
|
375
|
+
user = get_request_user(request)
|
|
375
376
|
|
|
376
377
|
if user is None:
|
|
377
378
|
raise HTTPException(
|
core/auth/helpers.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Helper functions for authentication.
|
|
3
|
+
|
|
4
|
+
Provides consistent user access across the framework, supporting both:
|
|
5
|
+
- request.user (Starlette AuthenticationMiddleware pattern - preferred)
|
|
6
|
+
- request.state.user (legacy pattern - backward compatibility)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from typing import Any, TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from starlette.requests import Request
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger("core.auth")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_request_user(request: "Request") -> Any | None:
|
|
21
|
+
"""
|
|
22
|
+
Get authenticated user from request.
|
|
23
|
+
|
|
24
|
+
Checks both patterns for compatibility:
|
|
25
|
+
1. request.user (Starlette AuthenticationMiddleware - preferred)
|
|
26
|
+
2. request.state.user (legacy pattern)
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
request: The Starlette/FastAPI request object
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
The authenticated user model or None if not authenticated
|
|
33
|
+
|
|
34
|
+
Example:
|
|
35
|
+
from core.auth.helpers import get_request_user
|
|
36
|
+
|
|
37
|
+
async def my_view(request: Request):
|
|
38
|
+
user = get_request_user(request)
|
|
39
|
+
if user is None:
|
|
40
|
+
raise HTTPException(401, "Not authenticated")
|
|
41
|
+
"""
|
|
42
|
+
# Pattern 1: request.user (Starlette AuthenticationMiddleware)
|
|
43
|
+
user = getattr(request, "user", None)
|
|
44
|
+
if user is not None:
|
|
45
|
+
# Check if it's an authenticated user (has is_authenticated = True)
|
|
46
|
+
if getattr(user, "is_authenticated", False):
|
|
47
|
+
# If it's our AuthenticatedUser wrapper, return the underlying model
|
|
48
|
+
if hasattr(user, "_user"):
|
|
49
|
+
return user._user
|
|
50
|
+
return user
|
|
51
|
+
|
|
52
|
+
# Pattern 2: request.state.user (legacy)
|
|
53
|
+
user = getattr(request.state, "user", None) if hasattr(request, "state") else None
|
|
54
|
+
|
|
55
|
+
return user
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def is_authenticated(request: "Request") -> bool:
|
|
59
|
+
"""
|
|
60
|
+
Check if request has an authenticated user.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
request: The Starlette/FastAPI request object
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
True if authenticated, False otherwise
|
|
67
|
+
"""
|
|
68
|
+
# Pattern 1: request.user with is_authenticated
|
|
69
|
+
user = getattr(request, "user", None)
|
|
70
|
+
if user is not None and getattr(user, "is_authenticated", False):
|
|
71
|
+
return True
|
|
72
|
+
|
|
73
|
+
# Pattern 2: request.state.user
|
|
74
|
+
if hasattr(request, "state"):
|
|
75
|
+
user = getattr(request.state, "user", None)
|
|
76
|
+
if user is not None:
|
|
77
|
+
return True
|
|
78
|
+
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def set_request_user(request: "Request", user: Any | None) -> None:
|
|
83
|
+
"""
|
|
84
|
+
Set the authenticated user on the request.
|
|
85
|
+
|
|
86
|
+
Sets both patterns for maximum compatibility:
|
|
87
|
+
- request.state.user (for dependencies and legacy code)
|
|
88
|
+
|
|
89
|
+
Note: request.user is set by Starlette's AuthenticationMiddleware
|
|
90
|
+
via scope["user"] and cannot be set directly.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
request: The Starlette/FastAPI request object
|
|
94
|
+
user: The user model or None
|
|
95
|
+
"""
|
|
96
|
+
if hasattr(request, "state"):
|
|
97
|
+
request.state.user = user
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
__all__ = [
|
|
101
|
+
"get_request_user",
|
|
102
|
+
"is_authenticated",
|
|
103
|
+
"set_request_user",
|
|
104
|
+
]
|
core/auth/middleware.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Authentication Middleware for Core Framework.
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
Uses Starlette's AuthenticationMiddleware pattern which correctly propagates
|
|
5
|
+
user to views via scope["user"] (accessed as request.user).
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
authentication backend and populates request.state.user.
|
|
7
|
+
IMPORTANT: Use request.user (not request.state.user) in your views!
|
|
8
8
|
|
|
9
9
|
Usage:
|
|
10
10
|
from core.auth.middleware import AuthenticationMiddleware
|
|
@@ -13,78 +13,117 @@ Usage:
|
|
|
13
13
|
middlewares=[(AuthenticationMiddleware, {})],
|
|
14
14
|
)
|
|
15
15
|
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
The middleware will:
|
|
24
|
-
1. Extract Bearer token from Authorization header
|
|
25
|
-
2. Validate the token
|
|
26
|
-
3. Fetch the user from database
|
|
27
|
-
4. Set request.state.user to the authenticated user (or None)
|
|
16
|
+
# In your view - use request.user
|
|
17
|
+
@router.get("/me")
|
|
18
|
+
async def me(request: Request):
|
|
19
|
+
if not request.user.is_authenticated:
|
|
20
|
+
raise HTTPException(401, "Not authenticated")
|
|
21
|
+
return {"id": request.user.id, "email": request.user.email}
|
|
28
22
|
"""
|
|
29
23
|
|
|
30
24
|
from __future__ import annotations
|
|
31
25
|
|
|
26
|
+
import logging
|
|
32
27
|
from typing import Any, TYPE_CHECKING
|
|
28
|
+
from uuid import UUID
|
|
33
29
|
|
|
34
|
-
from starlette.
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
from starlette.authentication import (
|
|
31
|
+
AuthenticationBackend,
|
|
32
|
+
AuthCredentials,
|
|
33
|
+
BaseUser,
|
|
34
|
+
UnauthenticatedUser,
|
|
35
|
+
)
|
|
36
|
+
from starlette.middleware.authentication import AuthenticationMiddleware as StarletteAuthMiddleware
|
|
37
|
+
from starlette.requests import HTTPConnection
|
|
38
|
+
|
|
39
|
+
from core.exceptions import (
|
|
40
|
+
InvalidToken,
|
|
41
|
+
TokenExpired,
|
|
42
|
+
UserNotFound,
|
|
43
|
+
UserInactive,
|
|
44
|
+
DatabaseException,
|
|
45
|
+
ConfigurationError,
|
|
46
|
+
)
|
|
37
47
|
|
|
38
48
|
if TYPE_CHECKING:
|
|
39
|
-
from
|
|
49
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
50
|
+
|
|
51
|
+
# Logger for authentication - NEVER silent!
|
|
52
|
+
logger = logging.getLogger("core.auth")
|
|
40
53
|
|
|
41
54
|
|
|
42
|
-
|
|
55
|
+
# =============================================================================
|
|
56
|
+
# Authenticated User Wrapper
|
|
57
|
+
# =============================================================================
|
|
58
|
+
|
|
59
|
+
class AuthenticatedUser(BaseUser):
|
|
43
60
|
"""
|
|
44
|
-
|
|
61
|
+
Wrapper for authenticated user that implements Starlette's BaseUser.
|
|
45
62
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
2. Extracts Bearer token from Authorization header
|
|
49
|
-
3. Validates the token using configured backend
|
|
50
|
-
4. Fetches user from database
|
|
51
|
-
5. Sets request.state.user to authenticated user
|
|
63
|
+
Provides access to the underlying database user model while implementing
|
|
64
|
+
the required Starlette interface.
|
|
52
65
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
Usage in views:
|
|
67
|
+
user = request.user
|
|
68
|
+
if user.is_authenticated:
|
|
69
|
+
print(user.email) # Access model attributes
|
|
70
|
+
print(user.id) # Access model attributes
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(self, user: Any) -> None:
|
|
74
|
+
self._user = user
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def is_authenticated(self) -> bool:
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def display_name(self) -> str:
|
|
82
|
+
return getattr(self._user, "email", str(self._user))
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def identity(self) -> str:
|
|
86
|
+
return str(getattr(self._user, "id", ""))
|
|
87
|
+
|
|
88
|
+
def __getattr__(self, name: str) -> Any:
|
|
89
|
+
"""Proxy attribute access to underlying user model."""
|
|
90
|
+
return getattr(self._user, name)
|
|
91
|
+
|
|
92
|
+
def __repr__(self) -> str:
|
|
93
|
+
return f"<AuthenticatedUser {self.display_name}>"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# =============================================================================
|
|
97
|
+
# Authentication Backend
|
|
98
|
+
# =============================================================================
|
|
99
|
+
|
|
100
|
+
class JWTAuthBackend(AuthenticationBackend):
|
|
101
|
+
"""
|
|
102
|
+
JWT Authentication Backend for Starlette.
|
|
103
|
+
|
|
104
|
+
This backend:
|
|
105
|
+
1. Extracts Bearer token from Authorization header
|
|
106
|
+
2. Verifies the token using core.auth.tokens
|
|
107
|
+
3. Fetches user from database
|
|
108
|
+
4. Returns AuthCredentials and AuthenticatedUser
|
|
109
|
+
|
|
110
|
+
IMPORTANT: All errors are logged, NEVER silenced!
|
|
67
111
|
|
|
68
|
-
Configuration
|
|
112
|
+
Configuration:
|
|
69
113
|
- user_model: User model class (uses global config if None)
|
|
70
114
|
- header_name: Header to extract token from (default: "Authorization")
|
|
71
115
|
- scheme: Expected scheme (default: "Bearer")
|
|
72
|
-
- skip_paths: List of paths to skip authentication (e.g., ["/health"])
|
|
73
116
|
"""
|
|
74
117
|
|
|
75
118
|
def __init__(
|
|
76
119
|
self,
|
|
77
|
-
app: "Callable[[Request], Awaitable[Response]]",
|
|
78
120
|
user_model: type | None = None,
|
|
79
121
|
header_name: str = "Authorization",
|
|
80
122
|
scheme: str = "Bearer",
|
|
81
|
-
skip_paths: list[str] | None = None,
|
|
82
123
|
) -> None:
|
|
83
|
-
super().__init__(app)
|
|
84
124
|
self._user_model = user_model
|
|
85
125
|
self.header_name = header_name
|
|
86
126
|
self.scheme = scheme
|
|
87
|
-
self.skip_paths = skip_paths or []
|
|
88
127
|
|
|
89
128
|
@property
|
|
90
129
|
def user_model(self) -> type | None:
|
|
@@ -95,139 +134,181 @@ class AuthenticationMiddleware(BaseHTTPMiddleware):
|
|
|
95
134
|
try:
|
|
96
135
|
from core.auth.models import get_user_model
|
|
97
136
|
return get_user_model()
|
|
98
|
-
except Exception:
|
|
137
|
+
except Exception as e:
|
|
138
|
+
logger.error(f"Failed to get user model: {e}")
|
|
99
139
|
return None
|
|
100
140
|
|
|
101
|
-
async def
|
|
102
|
-
self,
|
|
103
|
-
request: Request,
|
|
104
|
-
call_next: "Callable[[Request], Awaitable[Response]]",
|
|
105
|
-
) -> Response:
|
|
141
|
+
async def authenticate(self, conn: HTTPConnection) -> tuple[AuthCredentials, BaseUser] | None:
|
|
106
142
|
"""
|
|
107
|
-
|
|
143
|
+
Authenticate the request.
|
|
108
144
|
|
|
109
|
-
|
|
145
|
+
Returns:
|
|
146
|
+
Tuple of (AuthCredentials, AuthenticatedUser) if authenticated
|
|
147
|
+
None if no credentials provided
|
|
148
|
+
|
|
149
|
+
Note: Returns None for missing credentials, but LOGS all errors!
|
|
110
150
|
"""
|
|
111
|
-
#
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
# Skip authentication for configured paths
|
|
115
|
-
if self._should_skip(request.url.path):
|
|
116
|
-
return await call_next(request)
|
|
117
|
-
|
|
118
|
-
# Try to authenticate
|
|
119
|
-
try:
|
|
120
|
-
user = await self._authenticate(request)
|
|
121
|
-
request.state.user = user
|
|
122
|
-
except Exception:
|
|
123
|
-
# Authentication failed, keep user as None
|
|
124
|
-
pass
|
|
125
|
-
|
|
126
|
-
return await call_next(request)
|
|
127
|
-
|
|
128
|
-
def _should_skip(self, path: str) -> bool:
|
|
129
|
-
"""Check if path should skip authentication."""
|
|
130
|
-
for skip_path in self.skip_paths:
|
|
131
|
-
if path.startswith(skip_path):
|
|
132
|
-
return True
|
|
133
|
-
return False
|
|
134
|
-
|
|
135
|
-
def _extract_token(self, request: Request) -> str | None:
|
|
136
|
-
"""Extract token from Authorization header."""
|
|
137
|
-
auth_header = request.headers.get(self.header_name)
|
|
151
|
+
# Extract token from header
|
|
152
|
+
auth_header = conn.headers.get(self.header_name)
|
|
138
153
|
|
|
139
154
|
if not auth_header:
|
|
155
|
+
logger.debug("No Authorization header present")
|
|
140
156
|
return None
|
|
141
157
|
|
|
158
|
+
# Parse header
|
|
142
159
|
parts = auth_header.split()
|
|
143
|
-
|
|
144
160
|
if len(parts) != 2:
|
|
161
|
+
logger.warning(f"Malformed Authorization header: expected 2 parts, got {len(parts)}")
|
|
145
162
|
return None
|
|
146
163
|
|
|
147
164
|
scheme, token = parts
|
|
148
|
-
|
|
149
165
|
if scheme.lower() != self.scheme.lower():
|
|
166
|
+
logger.warning(f"Unexpected auth scheme: expected '{self.scheme}', got '{scheme}'")
|
|
150
167
|
return None
|
|
151
168
|
|
|
152
|
-
|
|
169
|
+
logger.debug(f"Token extracted: {token[:20]}...")
|
|
170
|
+
|
|
171
|
+
# Verify token
|
|
172
|
+
try:
|
|
173
|
+
user = await self._verify_and_get_user(token)
|
|
174
|
+
if user is None:
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
logger.info(f"User authenticated: {getattr(user, 'email', user)}")
|
|
178
|
+
return AuthCredentials(["authenticated"]), AuthenticatedUser(user)
|
|
179
|
+
|
|
180
|
+
except InvalidToken as e:
|
|
181
|
+
logger.warning(f"Invalid token: {e.message}")
|
|
182
|
+
return None
|
|
183
|
+
except TokenExpired as e:
|
|
184
|
+
logger.warning(f"Token expired: {e.message}")
|
|
185
|
+
return None
|
|
186
|
+
except UserNotFound as e:
|
|
187
|
+
logger.warning(f"User not found: {e.message}")
|
|
188
|
+
return None
|
|
189
|
+
except UserInactive as e:
|
|
190
|
+
logger.warning(f"User inactive: {e.message}")
|
|
191
|
+
return None
|
|
192
|
+
except DatabaseException as e:
|
|
193
|
+
logger.error(f"Database error during authentication: {e.message}", exc_info=True)
|
|
194
|
+
raise # Re-raise database errors - these are critical!
|
|
195
|
+
except ConfigurationError as e:
|
|
196
|
+
logger.error(f"Configuration error: {e.message}", exc_info=True)
|
|
197
|
+
raise # Re-raise configuration errors - these need to be fixed!
|
|
198
|
+
except Exception as e:
|
|
199
|
+
# Log unexpected errors with full stack trace
|
|
200
|
+
logger.exception(f"Unexpected error during authentication: {e}")
|
|
201
|
+
raise # NEVER silence unexpected errors!
|
|
153
202
|
|
|
154
|
-
async def
|
|
203
|
+
async def _verify_and_get_user(self, token: str) -> Any | None:
|
|
155
204
|
"""
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
205
|
+
Verify token and fetch user from database.
|
|
206
|
+
|
|
207
|
+
Raises:
|
|
208
|
+
InvalidToken: If token is invalid or malformed
|
|
209
|
+
TokenExpired: If token has expired
|
|
210
|
+
UserNotFound: If user doesn't exist
|
|
211
|
+
UserInactive: If user is inactive
|
|
212
|
+
DatabaseException: If database query fails
|
|
213
|
+
ConfigurationError: If auth is not properly configured
|
|
160
214
|
"""
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if not token:
|
|
164
|
-
return None
|
|
215
|
+
from core.auth.tokens import verify_token, decode_token
|
|
216
|
+
from core.auth.base import TokenError
|
|
165
217
|
|
|
166
218
|
# Verify token
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
219
|
+
try:
|
|
220
|
+
payload = verify_token(token, token_type="access")
|
|
221
|
+
except TokenError as e:
|
|
222
|
+
raise InvalidToken(f"Token verification failed: {e}")
|
|
170
223
|
|
|
171
224
|
if payload is None:
|
|
172
|
-
|
|
225
|
+
# verify_token returns None for various reasons - let's be more specific
|
|
226
|
+
try:
|
|
227
|
+
# Try to decode to get more info
|
|
228
|
+
raw_payload = decode_token(token)
|
|
229
|
+
token_type = raw_payload.get("type")
|
|
230
|
+
if token_type != "access":
|
|
231
|
+
raise InvalidToken(f"Token type mismatch: expected 'access', got '{token_type}'")
|
|
232
|
+
except Exception as e:
|
|
233
|
+
raise InvalidToken(f"Token decode failed: {e}")
|
|
234
|
+
raise InvalidToken("Token verification returned None")
|
|
235
|
+
|
|
236
|
+
logger.debug(f"Token payload: {payload}")
|
|
173
237
|
|
|
174
238
|
# Get user_id from token
|
|
175
239
|
user_id = payload.get("sub") or payload.get("user_id")
|
|
176
|
-
|
|
177
240
|
if not user_id:
|
|
178
|
-
|
|
241
|
+
raise InvalidToken("Token missing 'sub' or 'user_id' claim")
|
|
179
242
|
|
|
180
|
-
|
|
243
|
+
logger.debug(f"User ID from token: {user_id}")
|
|
244
|
+
|
|
245
|
+
# Get user model
|
|
181
246
|
User = self.user_model
|
|
182
247
|
if User is None:
|
|
183
|
-
|
|
248
|
+
raise ConfigurationError(
|
|
249
|
+
"No user model configured. "
|
|
250
|
+
"Set user_model in AuthenticationMiddleware or call configure_auth(user_model=...)"
|
|
251
|
+
)
|
|
184
252
|
|
|
185
|
-
#
|
|
186
|
-
# The middleware runs outside FastAPI DI context, so we need to
|
|
187
|
-
# handle database session creation carefully
|
|
253
|
+
# Get database session
|
|
188
254
|
db = await self._get_db_session()
|
|
189
255
|
if db is None:
|
|
190
|
-
|
|
256
|
+
raise DatabaseException(
|
|
257
|
+
"Could not obtain database session. "
|
|
258
|
+
"Ensure database is initialized with init_replicas() or database_url is set in settings."
|
|
259
|
+
)
|
|
191
260
|
|
|
192
261
|
try:
|
|
193
262
|
# Convert user_id to correct type
|
|
194
263
|
user_id_converted = self._convert_user_id(user_id, User)
|
|
264
|
+
logger.debug(f"User ID converted: {user_id_converted} (type: {type(user_id_converted).__name__})")
|
|
195
265
|
|
|
266
|
+
# Fetch user
|
|
196
267
|
user = await User.objects.using(db).filter(id=user_id_converted).first()
|
|
197
268
|
|
|
198
269
|
if user is None:
|
|
199
|
-
|
|
270
|
+
raise UserNotFound(f"User with id={user_id} not found")
|
|
200
271
|
|
|
201
272
|
# Check if user is active
|
|
202
273
|
if hasattr(user, "is_active") and not user.is_active:
|
|
203
|
-
|
|
274
|
+
raise UserInactive(f"User {user_id} is inactive")
|
|
204
275
|
|
|
276
|
+
logger.debug(f"User found: {getattr(user, 'email', user)}")
|
|
205
277
|
return user
|
|
206
|
-
|
|
207
|
-
|
|
278
|
+
|
|
279
|
+
except (UserNotFound, UserInactive):
|
|
280
|
+
raise # Re-raise our exceptions
|
|
281
|
+
except Exception as e:
|
|
282
|
+
raise DatabaseException(f"Database query failed: {e}")
|
|
208
283
|
finally:
|
|
209
284
|
await db.close()
|
|
210
285
|
|
|
211
|
-
async def _get_db_session(self) ->
|
|
286
|
+
async def _get_db_session(self) -> "AsyncSession | None":
|
|
212
287
|
"""
|
|
213
288
|
Get a database session for authentication.
|
|
214
289
|
|
|
215
|
-
|
|
216
|
-
Creates session directly from engine if normal path fails.
|
|
290
|
+
Tries multiple strategies and logs each attempt.
|
|
217
291
|
|
|
218
292
|
Returns:
|
|
219
|
-
AsyncSession or None if
|
|
293
|
+
AsyncSession or None if all strategies fail
|
|
220
294
|
"""
|
|
221
|
-
|
|
295
|
+
errors: list[str] = []
|
|
296
|
+
|
|
297
|
+
# Strategy 1: Use initialized session factory
|
|
222
298
|
try:
|
|
223
|
-
from core.database import
|
|
299
|
+
from core.database import _read_session_factory
|
|
224
300
|
|
|
225
301
|
if _read_session_factory is not None:
|
|
302
|
+
logger.debug("Using initialized session factory")
|
|
226
303
|
return _read_session_factory()
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
304
|
+
else:
|
|
305
|
+
errors.append("Session factory not initialized (init_replicas not called)")
|
|
306
|
+
except ImportError as e:
|
|
307
|
+
errors.append(f"Could not import database module: {e}")
|
|
308
|
+
except Exception as e:
|
|
309
|
+
errors.append(f"Session factory error: {e}")
|
|
310
|
+
|
|
311
|
+
# Strategy 2: Create session from settings
|
|
231
312
|
try:
|
|
232
313
|
from core.config import get_settings
|
|
233
314
|
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
|
|
@@ -236,12 +317,22 @@ class AuthenticationMiddleware(BaseHTTPMiddleware):
|
|
|
236
317
|
db_url = getattr(settings, 'database_read_url', None) or getattr(settings, 'database_url', None)
|
|
237
318
|
|
|
238
319
|
if db_url:
|
|
320
|
+
logger.debug(f"Creating session from settings: {db_url[:30]}...")
|
|
239
321
|
engine = create_async_engine(db_url, echo=False)
|
|
240
322
|
session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
|
241
323
|
return session_factory()
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
324
|
+
else:
|
|
325
|
+
errors.append("No database_url in settings")
|
|
326
|
+
except ImportError as e:
|
|
327
|
+
errors.append(f"Could not import config module: {e}")
|
|
328
|
+
except Exception as e:
|
|
329
|
+
errors.append(f"Settings engine error: {e}")
|
|
330
|
+
|
|
331
|
+
# All strategies failed - log detailed error
|
|
332
|
+
logger.error(
|
|
333
|
+
f"Could not obtain database session. Attempted strategies:\n" +
|
|
334
|
+
"\n".join(f" - {err}" for err in errors)
|
|
335
|
+
)
|
|
245
336
|
return None
|
|
246
337
|
|
|
247
338
|
def _convert_user_id(self, user_id: str, User: type) -> Any:
|
|
@@ -250,9 +341,7 @@ class AuthenticationMiddleware(BaseHTTPMiddleware):
|
|
|
250
341
|
|
|
251
342
|
Handles INTEGER, UUID, and string IDs.
|
|
252
343
|
"""
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
# Try to detect PK type
|
|
344
|
+
# Try to detect PK type from model
|
|
256
345
|
try:
|
|
257
346
|
from core.auth.models import _get_pk_column_type
|
|
258
347
|
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
|
|
@@ -261,86 +350,113 @@ class AuthenticationMiddleware(BaseHTTPMiddleware):
|
|
|
261
350
|
pk_type = _get_pk_column_type(User)
|
|
262
351
|
|
|
263
352
|
if pk_type == PG_UUID:
|
|
353
|
+
logger.debug(f"Converting user_id to UUID: {user_id}")
|
|
264
354
|
return UUID(user_id)
|
|
265
355
|
elif pk_type in (Integer, BigInteger):
|
|
356
|
+
logger.debug(f"Converting user_id to int: {user_id}")
|
|
266
357
|
return int(user_id)
|
|
267
|
-
except Exception:
|
|
268
|
-
|
|
358
|
+
except Exception as e:
|
|
359
|
+
logger.debug(f"Could not detect PK type, trying heuristics: {e}")
|
|
269
360
|
|
|
270
|
-
# Try UUID first
|
|
361
|
+
# Fallback: Try UUID first (common in modern systems)
|
|
271
362
|
try:
|
|
272
363
|
return UUID(user_id)
|
|
273
364
|
except (ValueError, TypeError):
|
|
274
365
|
pass
|
|
275
366
|
|
|
367
|
+
# Try int
|
|
276
368
|
try:
|
|
277
369
|
return int(user_id)
|
|
278
370
|
except (ValueError, TypeError):
|
|
279
371
|
pass
|
|
280
372
|
|
|
373
|
+
# Return as string
|
|
374
|
+
logger.debug(f"Using user_id as string: {user_id}")
|
|
281
375
|
return user_id
|
|
282
376
|
|
|
283
377
|
|
|
284
|
-
|
|
378
|
+
# =============================================================================
|
|
379
|
+
# Middleware Factory
|
|
380
|
+
# =============================================================================
|
|
381
|
+
|
|
382
|
+
class AuthenticationMiddleware(StarletteAuthMiddleware):
|
|
285
383
|
"""
|
|
286
|
-
|
|
384
|
+
Authentication Middleware using Starlette's proper pattern.
|
|
287
385
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
"""
|
|
386
|
+
This middleware correctly propagates user to views via request.user
|
|
387
|
+
(not request.state.user which doesn't work with BaseHTTPMiddleware).
|
|
291
388
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
) -> Response:
|
|
297
|
-
"""Process request, never failing on auth errors."""
|
|
298
|
-
request.state.user = None
|
|
389
|
+
Usage:
|
|
390
|
+
app = CoreApp(
|
|
391
|
+
middlewares=[(AuthenticationMiddleware, {})],
|
|
392
|
+
)
|
|
299
393
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
user = await self._authenticate(request)
|
|
303
|
-
request.state.user = user
|
|
304
|
-
except Exception:
|
|
305
|
-
pass # Silently ignore all errors
|
|
394
|
+
# Or with shortcut:
|
|
395
|
+
app = CoreApp(middleware=["auth"])
|
|
306
396
|
|
|
307
|
-
|
|
397
|
+
# In views, use request.user:
|
|
398
|
+
@router.get("/me")
|
|
399
|
+
async def me(request: Request):
|
|
400
|
+
if not request.user.is_authenticated:
|
|
401
|
+
raise HTTPException(401, "Not authenticated")
|
|
402
|
+
return {"email": request.user.email}
|
|
403
|
+
|
|
404
|
+
Note: Also sets request.state.user for backward compatibility.
|
|
405
|
+
"""
|
|
406
|
+
|
|
407
|
+
def __init__(
|
|
408
|
+
self,
|
|
409
|
+
app: Any,
|
|
410
|
+
user_model: type | None = None,
|
|
411
|
+
header_name: str = "Authorization",
|
|
412
|
+
scheme: str = "Bearer",
|
|
413
|
+
on_error: Any = None,
|
|
414
|
+
) -> None:
|
|
415
|
+
backend = JWTAuthBackend(
|
|
416
|
+
user_model=user_model,
|
|
417
|
+
header_name=header_name,
|
|
418
|
+
scheme=scheme,
|
|
419
|
+
)
|
|
420
|
+
super().__init__(app, backend=backend, on_error=on_error)
|
|
421
|
+
logger.info("AuthenticationMiddleware initialized")
|
|
308
422
|
|
|
309
423
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
424
|
+
class OptionalAuthenticationMiddleware(AuthenticationMiddleware):
|
|
425
|
+
"""
|
|
426
|
+
Same as AuthenticationMiddleware but doesn't require authentication.
|
|
427
|
+
|
|
428
|
+
Useful for endpoints that work both with and without authentication.
|
|
429
|
+
User will be UnauthenticatedUser if no valid token provided.
|
|
430
|
+
"""
|
|
431
|
+
pass
|
|
313
432
|
|
|
314
|
-
_middleware_registered = False
|
|
315
433
|
|
|
434
|
+
# =============================================================================
|
|
435
|
+
# Legacy Compatibility
|
|
436
|
+
# =============================================================================
|
|
316
437
|
|
|
317
438
|
def ensure_auth_middleware(app: Any) -> None:
|
|
318
439
|
"""
|
|
319
440
|
Ensure AuthenticationMiddleware is registered on the app.
|
|
320
441
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
Args:
|
|
324
|
-
app: FastAPI or CoreApp instance
|
|
442
|
+
DEPRECATED: Use middleware=["auth"] instead.
|
|
325
443
|
"""
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
# Try to add middleware
|
|
444
|
+
logger.warning(
|
|
445
|
+
"ensure_auth_middleware is deprecated. "
|
|
446
|
+
"Use middleware=['auth'] or add AuthenticationMiddleware directly."
|
|
447
|
+
)
|
|
332
448
|
try:
|
|
333
449
|
if hasattr(app, "add_middleware"):
|
|
334
450
|
app.add_middleware(AuthenticationMiddleware)
|
|
335
|
-
|
|
336
|
-
except Exception:
|
|
337
|
-
|
|
451
|
+
logger.info("AuthenticationMiddleware added to app")
|
|
452
|
+
except Exception as e:
|
|
453
|
+
logger.error(f"Failed to add AuthenticationMiddleware: {e}")
|
|
454
|
+
raise
|
|
338
455
|
|
|
339
456
|
|
|
340
457
|
def reset_middleware_state() -> None:
|
|
341
|
-
"""Reset middleware
|
|
342
|
-
|
|
343
|
-
_middleware_registered = False
|
|
458
|
+
"""Reset middleware state (for testing)."""
|
|
459
|
+
pass # No longer needed with new implementation
|
|
344
460
|
|
|
345
461
|
|
|
346
462
|
# =============================================================================
|
|
@@ -350,6 +466,8 @@ def reset_middleware_state() -> None:
|
|
|
350
466
|
__all__ = [
|
|
351
467
|
"AuthenticationMiddleware",
|
|
352
468
|
"OptionalAuthenticationMiddleware",
|
|
469
|
+
"JWTAuthBackend",
|
|
470
|
+
"AuthenticatedUser",
|
|
353
471
|
"ensure_auth_middleware",
|
|
354
472
|
"reset_middleware_state",
|
|
355
473
|
]
|
core/auth/tokens.py
CHANGED
|
@@ -18,6 +18,7 @@ Uso:
|
|
|
18
18
|
|
|
19
19
|
from __future__ import annotations
|
|
20
20
|
|
|
21
|
+
import logging
|
|
21
22
|
from datetime import timedelta
|
|
22
23
|
from typing import Any
|
|
23
24
|
|
|
@@ -29,6 +30,9 @@ from core.auth.base import (
|
|
|
29
30
|
)
|
|
30
31
|
from core.datetime import timezone
|
|
31
32
|
|
|
33
|
+
# Logger for token operations
|
|
34
|
+
logger = logging.getLogger("core.auth.tokens")
|
|
35
|
+
|
|
32
36
|
|
|
33
37
|
class JWTBackend(TokenBackend):
|
|
34
38
|
"""
|
|
@@ -123,10 +127,14 @@ class JWTBackend(TokenBackend):
|
|
|
123
127
|
import jwt
|
|
124
128
|
|
|
125
129
|
try:
|
|
126
|
-
|
|
130
|
+
payload = jwt.decode(token, self.secret_key, algorithms=[self.algorithm])
|
|
131
|
+
logger.debug(f"Token decoded: sub={payload.get('sub')}, type={payload.get('type')}")
|
|
132
|
+
return payload
|
|
127
133
|
except jwt.ExpiredSignatureError:
|
|
134
|
+
logger.debug("Token decode failed: token expired")
|
|
128
135
|
raise TokenError("Token expired")
|
|
129
136
|
except jwt.InvalidTokenError as e:
|
|
137
|
+
logger.debug(f"Token decode failed: {e}")
|
|
130
138
|
raise TokenError(f"Invalid token: {e}")
|
|
131
139
|
|
|
132
140
|
def verify_token(
|
|
@@ -147,11 +155,17 @@ class JWTBackend(TokenBackend):
|
|
|
147
155
|
try:
|
|
148
156
|
payload = self.decode_token(token)
|
|
149
157
|
|
|
150
|
-
|
|
158
|
+
actual_type = payload.get("type")
|
|
159
|
+
if actual_type != token_type:
|
|
160
|
+
logger.debug(
|
|
161
|
+
f"Token type mismatch: expected '{token_type}', got '{actual_type}'"
|
|
162
|
+
)
|
|
151
163
|
return None
|
|
152
164
|
|
|
165
|
+
logger.debug(f"Token verified successfully: sub={payload.get('sub')}")
|
|
153
166
|
return payload
|
|
154
|
-
except TokenError:
|
|
167
|
+
except TokenError as e:
|
|
168
|
+
logger.debug(f"Token verification failed: {e}")
|
|
155
169
|
return None
|
|
156
170
|
|
|
157
171
|
def refresh_token(self, refresh_token: str) -> tuple[str, str] | None:
|
core/auth/views.py
CHANGED
|
@@ -360,15 +360,27 @@ class AuthViewSet(ViewSet):
|
|
|
360
360
|
) -> dict:
|
|
361
361
|
"""
|
|
362
362
|
Get current authenticated user.
|
|
363
|
+
|
|
364
|
+
Uses request.user (Starlette pattern) with fallback to request.state.user
|
|
365
|
+
for backward compatibility.
|
|
363
366
|
"""
|
|
367
|
+
# Try request.user first (Starlette AuthenticationMiddleware pattern)
|
|
368
|
+
user = getattr(request, "user", None)
|
|
369
|
+
if user is not None and getattr(user, "is_authenticated", False):
|
|
370
|
+
# user is an AuthenticatedUser wrapper - get underlying model
|
|
371
|
+
if hasattr(user, "_user"):
|
|
372
|
+
user = user._user
|
|
373
|
+
return self.user_output_schema.model_validate(user).model_dump()
|
|
374
|
+
|
|
375
|
+
# Fallback to request.state.user (legacy pattern)
|
|
364
376
|
user = getattr(request.state, "user", None)
|
|
365
|
-
if user is None:
|
|
366
|
-
|
|
367
|
-
status_code=401,
|
|
368
|
-
detail="Not authenticated"
|
|
369
|
-
)
|
|
377
|
+
if user is not None:
|
|
378
|
+
return self.user_output_schema.model_validate(user).model_dump()
|
|
370
379
|
|
|
371
|
-
|
|
380
|
+
raise HTTPException(
|
|
381
|
+
status_code=401,
|
|
382
|
+
detail="Not authenticated"
|
|
383
|
+
)
|
|
372
384
|
|
|
373
385
|
@action(methods=["POST"], detail=False, permission_classes=[IsAuthenticated])
|
|
374
386
|
async def change_password(
|
|
@@ -381,7 +393,15 @@ class AuthViewSet(ViewSet):
|
|
|
381
393
|
"""
|
|
382
394
|
Change password for current user.
|
|
383
395
|
"""
|
|
384
|
-
|
|
396
|
+
# Try request.user first (Starlette pattern)
|
|
397
|
+
user = getattr(request, "user", None)
|
|
398
|
+
if user is not None and getattr(user, "is_authenticated", False):
|
|
399
|
+
if hasattr(user, "_user"):
|
|
400
|
+
user = user._user
|
|
401
|
+
else:
|
|
402
|
+
# Fallback to request.state.user
|
|
403
|
+
user = getattr(request.state, "user", None)
|
|
404
|
+
|
|
385
405
|
if user is None:
|
|
386
406
|
raise HTTPException(
|
|
387
407
|
status_code=401,
|
core/cli/main.py
CHANGED
|
@@ -1039,9 +1039,10 @@ class AuthViewSet(ModelViewSet):
|
|
|
1039
1039
|
Returns:
|
|
1040
1040
|
Current user data
|
|
1041
1041
|
"""
|
|
1042
|
-
# User is available via request.
|
|
1042
|
+
# User is available via request.user (Starlette pattern) or request.state.user (legacy)
|
|
1043
1043
|
# permission_classes=[IsAuthenticated] ensures user is authenticated
|
|
1044
|
-
|
|
1044
|
+
from core.auth.helpers import get_request_user
|
|
1045
|
+
user = get_request_user(request)
|
|
1045
1046
|
|
|
1046
1047
|
if user is None:
|
|
1047
1048
|
raise HTTPException(status_code=401, detail="Authentication required")
|
|
@@ -1065,9 +1066,10 @@ class AuthViewSet(ModelViewSet):
|
|
|
1065
1066
|
body = await request.json()
|
|
1066
1067
|
data = ChangePasswordInput.model_validate(body)
|
|
1067
1068
|
|
|
1068
|
-
# User is available via request.
|
|
1069
|
+
# User is available via request.user (Starlette pattern) or request.state.user (legacy)
|
|
1069
1070
|
# permission_classes=[IsAuthenticated] ensures user is authenticated
|
|
1070
|
-
|
|
1071
|
+
from core.auth.helpers import get_request_user
|
|
1072
|
+
user = get_request_user(request)
|
|
1071
1073
|
|
|
1072
1074
|
if user is None:
|
|
1073
1075
|
raise HTTPException(status_code=401, detail="Authentication required")
|
core/dependencies.py
CHANGED
|
@@ -395,12 +395,14 @@ async def get_request_context(request: Request) -> dict[str, Any]:
|
|
|
395
395
|
|
|
396
396
|
Útil para logging e auditoria.
|
|
397
397
|
"""
|
|
398
|
+
from core.auth.helpers import get_request_user
|
|
399
|
+
|
|
398
400
|
return {
|
|
399
401
|
"method": request.method,
|
|
400
402
|
"url": str(request.url),
|
|
401
403
|
"client_ip": request.client.host if request.client else None,
|
|
402
404
|
"user_agent": request.headers.get("user-agent"),
|
|
403
|
-
"user":
|
|
405
|
+
"user": get_request_user(request),
|
|
404
406
|
}
|
|
405
407
|
|
|
406
408
|
|
core/permissions.py
CHANGED
|
@@ -15,6 +15,8 @@ from typing import Any, TYPE_CHECKING
|
|
|
15
15
|
|
|
16
16
|
from fastapi import HTTPException, Request, status
|
|
17
17
|
|
|
18
|
+
from core.auth.helpers import get_request_user
|
|
19
|
+
|
|
18
20
|
if TYPE_CHECKING:
|
|
19
21
|
from core.views import APIView
|
|
20
22
|
|
|
@@ -32,7 +34,7 @@ class Permission(ABC):
|
|
|
32
34
|
request: Request,
|
|
33
35
|
view: APIView | None = None,
|
|
34
36
|
) -> bool:
|
|
35
|
-
user =
|
|
37
|
+
user = get_request_user(request)
|
|
36
38
|
return user is not None and user.is_admin
|
|
37
39
|
"""
|
|
38
40
|
|
|
@@ -214,7 +216,7 @@ class IsAuthenticated(Permission):
|
|
|
214
216
|
request: Request,
|
|
215
217
|
view: "APIView | None" = None,
|
|
216
218
|
) -> bool:
|
|
217
|
-
user =
|
|
219
|
+
user = get_request_user(request)
|
|
218
220
|
return user is not None
|
|
219
221
|
|
|
220
222
|
|
|
@@ -236,7 +238,7 @@ class IsAuthenticatedOrReadOnly(Permission):
|
|
|
236
238
|
if request.method in self.SAFE_METHODS:
|
|
237
239
|
return True
|
|
238
240
|
|
|
239
|
-
user =
|
|
241
|
+
user = get_request_user(request)
|
|
240
242
|
return user is not None
|
|
241
243
|
|
|
242
244
|
|
|
@@ -250,7 +252,7 @@ class IsAdmin(Permission):
|
|
|
250
252
|
request: Request,
|
|
251
253
|
view: "APIView | None" = None,
|
|
252
254
|
) -> bool:
|
|
253
|
-
user =
|
|
255
|
+
user = get_request_user(request)
|
|
254
256
|
if user is None:
|
|
255
257
|
return False
|
|
256
258
|
|
|
@@ -285,7 +287,7 @@ class IsOwner(Permission):
|
|
|
285
287
|
if obj is None:
|
|
286
288
|
return True
|
|
287
289
|
|
|
288
|
-
user =
|
|
290
|
+
user = get_request_user(request)
|
|
289
291
|
if user is None:
|
|
290
292
|
return False
|
|
291
293
|
|
|
@@ -318,7 +320,7 @@ class HasRole(Permission):
|
|
|
318
320
|
request: Request,
|
|
319
321
|
view: "APIView | None" = None,
|
|
320
322
|
) -> bool:
|
|
321
|
-
user =
|
|
323
|
+
user = get_request_user(request)
|
|
322
324
|
if user is None:
|
|
323
325
|
return False
|
|
324
326
|
|
core/tenancy.py
CHANGED
|
@@ -231,8 +231,9 @@ async def extract_tenant_from_request(
|
|
|
231
231
|
|
|
232
232
|
Checks user, header, and query param in order.
|
|
233
233
|
"""
|
|
234
|
-
|
|
235
|
-
|
|
234
|
+
from core.auth.helpers import get_request_user
|
|
235
|
+
|
|
236
|
+
user = get_request_user(request)
|
|
236
237
|
if user is not None:
|
|
237
238
|
tenant_id = getattr(user, user_tenant_attr, None)
|
|
238
239
|
if tenant_id is not None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: core-framework
|
|
3
|
-
Version: 0.12.
|
|
3
|
+
Version: 0.12.6
|
|
4
4
|
Summary: Core Framework - Django-inspired, FastAPI-powered. Alta performance, baixo acoplamento, produtividade extrema.
|
|
5
5
|
Project-URL: Homepage, https://github.com/SorPuti/core-framework
|
|
6
6
|
Project-URL: Documentation, https://github.com/SorPuti/core-framework#readme
|
|
@@ -1,35 +1,36 @@
|
|
|
1
|
-
core/__init__.py,sha256=
|
|
1
|
+
core/__init__.py,sha256=vm-wiEtrmCVt8WjZQ0iEghsZqmiC5CYPifbNP_BY8kk,12058
|
|
2
2
|
core/app.py,sha256=sCA3mJI696i7MIjrPxfOr5zEYt0njarQfHHy3EAajk4,21071
|
|
3
3
|
core/choices.py,sha256=rhcL3p2dB7RK99zIilpmoTFVcibQEIaRpz0CY0kImCE,10502
|
|
4
4
|
core/config.py,sha256=2-MVF9nLoYmxpYYH_Gzn4-Sa3MU87YZskRPtlNyhg6Q,14049
|
|
5
5
|
core/database.py,sha256=XqB5tZnb9UYDbVGIh96YbmbGJZMqln6-diPBHCr3VWk,11564
|
|
6
6
|
core/datetime.py,sha256=bzqlAj3foA-lzbhXjlEiDNR2D-nwXu9mpxpdcUb-Pmw,32730
|
|
7
|
-
core/dependencies.py,sha256=
|
|
7
|
+
core/dependencies.py,sha256=p207A8qwj-QVAb7nNSe3HxkefClwSQNQQylSFFa-meU,11627
|
|
8
8
|
core/exceptions.py,sha256=cdcffeYnMzCbS4hApOYNmPVNbPUpKcrgJbi3nKhqTuI,22702
|
|
9
9
|
core/fields.py,sha256=F2NdToowkJ_LFvPN9KVyxIFES1AlVDy7WkEp-8UiBpA,9327
|
|
10
10
|
core/middleware.py,sha256=MZVw7smJ2MiycYvkaYIC2cNpyqYGk3m-eeoDandqZU4,23506
|
|
11
11
|
core/models.py,sha256=jdNdjRPKBZiOBOgg8CmDYBwmuWdD7twCIpqINLGY4Q4,33788
|
|
12
|
-
core/permissions.py,sha256=
|
|
12
|
+
core/permissions.py,sha256=HhIXtmK9nehfRpTLoxKaBA-g-aCWOlDpLigo06fsEew,10144
|
|
13
13
|
core/querysets.py,sha256=Z87-U06Un_xA9GKwcjXx0yzw6F_xf_tvG_rBT5UGL9c,22678
|
|
14
14
|
core/relations.py,sha256=UbdRgj0XQGI4lv2FQV1ImSAwu4Pn8yxTkSsdzR3m8cM,21372
|
|
15
15
|
core/routing.py,sha256=vIiJN8bQ2836WW2zUKTJVBTC8RpjtDYgEGdz7mldnGc,15422
|
|
16
16
|
core/serializers.py,sha256=gR5Y7wTACm1pECkUEpAKBUbPmONGLMDDwej4fyIiOdo,9438
|
|
17
|
-
core/tenancy.py,sha256=
|
|
17
|
+
core/tenancy.py,sha256=R4tNrLcAgRRDSqOvJS2IRXcD2J-zoCE4ng01ip9xWKI,9169
|
|
18
18
|
core/validators.py,sha256=LCDyvqwIKnMaUEdaVx5kWveZt3XsydknZ_bxBL4ic5U,27895
|
|
19
19
|
core/views.py,sha256=Vm2FREET0IJ2JZbClNJ0vvZ6RN5aQKC1sDXsrOb4-SY,43319
|
|
20
|
-
core/auth/__init__.py,sha256=
|
|
21
|
-
core/auth/backends.py,sha256=
|
|
20
|
+
core/auth/__init__.py,sha256=_yr4rMMvDt_uKujzkKfqlQZ6x9UiQ6jmRppw14hTQNc,4645
|
|
21
|
+
core/auth/backends.py,sha256=PkLk2RQN2rQdtYSiN0mn7cqSp95hnLjO9xTFZqSsPF8,10486
|
|
22
22
|
core/auth/base.py,sha256=Q7vXgwTmgdmyW7G8eJmDket2bKB_8YFnraZ_kK9_gTs,21425
|
|
23
|
-
core/auth/decorators.py,sha256=
|
|
23
|
+
core/auth/decorators.py,sha256=Db8ihM7E2d658500aZwYKoSPduXC5nPjkQLzPakc1Mc,12269
|
|
24
24
|
core/auth/hashers.py,sha256=0gIf67TU0k5H744FADpyh9_ugxA7m3mhYPZxLh_lEtc,12808
|
|
25
|
-
core/auth/
|
|
25
|
+
core/auth/helpers.py,sha256=nuaVL9KzIy6ZkVWz9Z327qe4-sfF0NbbJDFIX3tR6vg,3020
|
|
26
|
+
core/auth/middleware.py,sha256=3Wddxi2MKHrHguKfdW9LRKhHaHcmQ807edVcsFLqhVc,16645
|
|
26
27
|
core/auth/models.py,sha256=aEE7deQKPS1aH0Btzzh3Z1Bwuqy8zvLZwu4JFEmiUNk,34058
|
|
27
28
|
core/auth/permissions.py,sha256=v3ykAgNpq5wJ0NkuC_FuveMctOkDfM9Xp11XEnUAuBg,12461
|
|
28
29
|
core/auth/schemas.py,sha256=L0W96dOD348rJDGeu1K5Rz3aJj-GdwMr2vbwwsYfo2g,3469
|
|
29
|
-
core/auth/tokens.py,sha256=
|
|
30
|
-
core/auth/views.py,sha256=
|
|
30
|
+
core/auth/tokens.py,sha256=jOF40D5O8WRG8klRwMBuSG-jOhdsp1irXn2aZ2puNSg,9149
|
|
31
|
+
core/auth/views.py,sha256=3gMaq8pzWoWr29ExJk21JgGay2fE3Fq3Tz99Wb_ftvE,14203
|
|
31
32
|
core/cli/__init__.py,sha256=obodnvfe8DUziqpk-IAaHTEOb1KSfYQeuBZEAofut4o,449
|
|
32
|
-
core/cli/main.py,sha256=
|
|
33
|
+
core/cli/main.py,sha256=Odl-tetCDffs9xJ7l18xK4X583nGXYmD3nr-w-8cdZM,119787
|
|
33
34
|
core/deployment/__init__.py,sha256=RNcBRO9oB3WRnhtTTwM6wzVEcUKpKF4XfRkGSbbykIc,794
|
|
34
35
|
core/deployment/docker.py,sha256=ywraIk-ncbHiAX2vXH7jcU9KjhCGPIg7j0xgOTu5Cg8,8681
|
|
35
36
|
core/deployment/kubernetes.py,sha256=IV6gf664EFEyQry2ehgJ2UFhZhWovKpaHXln_0WXCMg,8414
|
|
@@ -78,7 +79,7 @@ example/auth.py,sha256=zBpLutb8lVKnGfQqQ2wnyygsSutHYZzeJBuhnFhxBaQ,4971
|
|
|
78
79
|
example/models.py,sha256=xKdx0kJ9n0tZ7sCce3KhV3BTvKvsh6m7G69eFm3ukf0,4549
|
|
79
80
|
example/schemas.py,sha256=wJ9QofnuHp4PjtM_IuMMBLVFVDJ4YlwcF6uQm1ooKiY,6139
|
|
80
81
|
example/views.py,sha256=GQwgQcW6yoeUIDbF7-lsaZV7cs8G1S1vGVtiwVpZIQE,14338
|
|
81
|
-
core_framework-0.12.
|
|
82
|
-
core_framework-0.12.
|
|
83
|
-
core_framework-0.12.
|
|
84
|
-
core_framework-0.12.
|
|
82
|
+
core_framework-0.12.6.dist-info/METADATA,sha256=LsQ-RfhBtAvm3GxcRMBLur-3lVBeig9b0pE3Hp_WXkg,12791
|
|
83
|
+
core_framework-0.12.6.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
84
|
+
core_framework-0.12.6.dist-info/entry_points.txt,sha256=lQ65IAOpieqU1VcHCUReeyandpyy8IKGix6IkJW_4Is,39
|
|
85
|
+
core_framework-0.12.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|