oxutils 0.1.12__py3-none-any.whl → 0.1.15__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.
- oxutils/__init__.py +1 -1
- oxutils/jwt/auth.py +39 -1
- oxutils/jwt/middleware.py +404 -0
- oxutils/jwt/models.py +98 -2
- oxutils/jwt/tokens.py +8 -0
- oxutils/oxiliere/middleware.py +46 -5
- oxutils/oxiliere/permissions.py +68 -47
- oxutils/oxiliere/schemas.py +31 -0
- oxutils/permissions/caches.py +17 -3
- oxutils/permissions/perms.py +106 -0
- oxutils/permissions/utils.py +178 -22
- oxutils/users/migrations/0003_user_photo.py +18 -0
- oxutils/users/models.py +1 -0
- {oxutils-0.1.12.dist-info → oxutils-0.1.15.dist-info}/METADATA +1 -1
- {oxutils-0.1.12.dist-info → oxutils-0.1.15.dist-info}/RECORD +16 -14
- {oxutils-0.1.12.dist-info → oxutils-0.1.15.dist-info}/WHEEL +0 -0
oxutils/__init__.py
CHANGED
oxutils/jwt/auth.py
CHANGED
|
@@ -17,6 +17,7 @@ from ninja_jwt.authentication import (
|
|
|
17
17
|
JWTStatelessUserAuthentication
|
|
18
18
|
)
|
|
19
19
|
from ninja.security import (
|
|
20
|
+
HttpBearer,
|
|
20
21
|
APIKeyCookie,
|
|
21
22
|
HttpBasicAuth,
|
|
22
23
|
)
|
|
@@ -176,6 +177,11 @@ class BasicAuth(HttpBasicAuth):
|
|
|
176
177
|
|
|
177
178
|
class BasicNoPasswordAuth(HttpBasicAuth):
|
|
178
179
|
def authenticate(self, request: HttpRequest, username: str, password: str) -> Optional[Any]:
|
|
180
|
+
|
|
181
|
+
# check if the middleware have already authenticated the user
|
|
182
|
+
if request.user.is_authenticated:
|
|
183
|
+
return request.user
|
|
184
|
+
|
|
179
185
|
try:
|
|
180
186
|
user = get_user_model().objects.get(email=username)
|
|
181
187
|
if user and user.is_active:
|
|
@@ -185,12 +191,34 @@ class BasicNoPasswordAuth(HttpBasicAuth):
|
|
|
185
191
|
except Exception as e:
|
|
186
192
|
return None
|
|
187
193
|
|
|
188
|
-
|
|
194
|
+
|
|
195
|
+
class JWTPassiveAuth(HttpBearer):
|
|
196
|
+
def authenticate(self, request: HttpRequest, token: str) -> Optional[Any]:
|
|
197
|
+
if request.user.is_authenticated:
|
|
198
|
+
return request.user
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class JWTCookiePassiveAuth(APIKeyCookie):
|
|
203
|
+
def authenticate(self, request: HttpRequest, key: Optional[str]) -> Optional[Any]:
|
|
204
|
+
if request.user.is_authenticated:
|
|
205
|
+
return request.user
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# for development Purpose only
|
|
189
210
|
basic_auth = BasicAuth()
|
|
190
211
|
basic_no_password_auth = BasicNoPasswordAuth()
|
|
212
|
+
|
|
213
|
+
# used on oxiliere authentication service
|
|
214
|
+
x_session_token_auth = XSessionTokenAuth()
|
|
191
215
|
jwt_auth = JWTAuth()
|
|
192
216
|
jwt_cookie_auth = JWTCookieAuth()
|
|
193
217
|
|
|
218
|
+
# Production in all oxiliere services
|
|
219
|
+
jwt_passive_auth = JWTPassiveAuth()
|
|
220
|
+
jwt_cookie_passive_auth = JWTCookiePassiveAuth()
|
|
221
|
+
|
|
194
222
|
|
|
195
223
|
|
|
196
224
|
|
|
@@ -202,3 +230,13 @@ def get_auth_handlers(auths: List[AuthBase] = []) -> List[AuthBase]:
|
|
|
202
230
|
return auths
|
|
203
231
|
|
|
204
232
|
return [jwt_auth, jwt_cookie_auth]
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def get_passive_auth_handlers(auths: List[AuthBase] = []) -> List[AuthBase]:
|
|
236
|
+
"""Passive auth handler switcher based on settings.DEBUG"""
|
|
237
|
+
from django.conf import settings
|
|
238
|
+
|
|
239
|
+
if settings.DEBUG:
|
|
240
|
+
return auths
|
|
241
|
+
|
|
242
|
+
return [jwt_passive_auth, jwt_cookie_passive_auth]
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
from typing import Type, Optional
|
|
2
|
+
import structlog
|
|
3
|
+
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from django.contrib.auth import get_user_model
|
|
6
|
+
from django.contrib.auth.models import AbstractBaseUser, AnonymousUser
|
|
7
|
+
from django.core.exceptions import PermissionDenied
|
|
8
|
+
from django.http import HttpRequest
|
|
9
|
+
from django.utils.translation import gettext_lazy as _
|
|
10
|
+
|
|
11
|
+
from ninja_jwt.exceptions import AuthenticationFailed, InvalidToken, TokenError
|
|
12
|
+
from ninja_jwt.settings import api_settings
|
|
13
|
+
from ninja_jwt.tokens import Token
|
|
14
|
+
from oxutils.constants import ACCESS_TOKEN_COOKIE
|
|
15
|
+
from ninja.utils import check_csrf
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
logger = structlog.get_logger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class JWTAuthBaseMiddleware:
|
|
24
|
+
"""
|
|
25
|
+
Base middleware for JWT authentication.
|
|
26
|
+
Handles token validation and user authentication.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, get_response):
|
|
30
|
+
self.get_response = get_response
|
|
31
|
+
self.user_model = get_user_model()
|
|
32
|
+
|
|
33
|
+
def __call__(self, request: HttpRequest):
|
|
34
|
+
"""
|
|
35
|
+
Process the request through the middleware.
|
|
36
|
+
"""
|
|
37
|
+
# Process request before view
|
|
38
|
+
self.process_request(request)
|
|
39
|
+
|
|
40
|
+
# Get response from next middleware/view
|
|
41
|
+
response = self.get_response(request)
|
|
42
|
+
|
|
43
|
+
return response
|
|
44
|
+
|
|
45
|
+
def get_token_from_request(self, request: HttpRequest) -> Optional[str]:
|
|
46
|
+
"""
|
|
47
|
+
Extract JWT token from request.
|
|
48
|
+
Must be implemented by subclasses.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Token string if found, None otherwise
|
|
52
|
+
"""
|
|
53
|
+
raise NotImplementedError(
|
|
54
|
+
"Subclasses must implement get_token_from_request() method"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def process_request(self, request: HttpRequest):
|
|
58
|
+
"""
|
|
59
|
+
Process request and validate JWT token if present.
|
|
60
|
+
Authentication is handled by another service - this only validates token signature and claims.
|
|
61
|
+
|
|
62
|
+
Skips authentication if user is already authenticated by a previous middleware.
|
|
63
|
+
"""
|
|
64
|
+
# Skip if user is already authenticated by another middleware
|
|
65
|
+
if hasattr(request, 'user') and request.user.is_authenticated:
|
|
66
|
+
if settings.DEBUG:
|
|
67
|
+
logger.debug(
|
|
68
|
+
"jwt_auth_skipped",
|
|
69
|
+
description="User already authenticated, skipping JWT middleware",
|
|
70
|
+
user_id=getattr(request.user, 'id', None),
|
|
71
|
+
path=request.path
|
|
72
|
+
)
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
token = self.get_token_from_request(request)
|
|
77
|
+
except PermissionDenied as e:
|
|
78
|
+
# CSRF or other security check failed
|
|
79
|
+
logger.warning(
|
|
80
|
+
"security_check_failed",
|
|
81
|
+
description="Security check failed during token extraction",
|
|
82
|
+
exception=type(e).__name__,
|
|
83
|
+
error=str(e),
|
|
84
|
+
path=request.path,
|
|
85
|
+
remote_addr=request.META.get('REMOTE_ADDR')
|
|
86
|
+
)
|
|
87
|
+
request.user = AnonymousUser()
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
if token:
|
|
91
|
+
try:
|
|
92
|
+
# Only validate token and extract user info (no DB lookup)
|
|
93
|
+
self.jwt_authenticate(request, token)
|
|
94
|
+
except (InvalidToken, AuthenticationFailed) as e:
|
|
95
|
+
# Token invalid - set anonymous user
|
|
96
|
+
if settings.DEBUG:
|
|
97
|
+
logger.debug(
|
|
98
|
+
"jwt_validation_failed",
|
|
99
|
+
description="JWT validation failed",
|
|
100
|
+
exception=type(e).__name__,
|
|
101
|
+
path=request.path
|
|
102
|
+
)
|
|
103
|
+
request.user = AnonymousUser()
|
|
104
|
+
else:
|
|
105
|
+
# No token provided - anonymous user
|
|
106
|
+
request.user = AnonymousUser()
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def get_validated_token(cls, raw_token) -> Type[Token]:
|
|
110
|
+
"""
|
|
111
|
+
Validates an encoded JSON web token and returns a validated token wrapper object.
|
|
112
|
+
Only validates signature and claims - authentication is handled by external service.
|
|
113
|
+
"""
|
|
114
|
+
messages = []
|
|
115
|
+
for AuthToken in api_settings.AUTH_TOKEN_CLASSES:
|
|
116
|
+
try:
|
|
117
|
+
return AuthToken(raw_token)
|
|
118
|
+
except TokenError as e:
|
|
119
|
+
messages.append(
|
|
120
|
+
{
|
|
121
|
+
"token_class": AuthToken.__name__,
|
|
122
|
+
"token_type": AuthToken.token_type,
|
|
123
|
+
"message": e.args[0],
|
|
124
|
+
}
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
raise InvalidToken(
|
|
128
|
+
{
|
|
129
|
+
"detail": _("Given token not valid for any token type"),
|
|
130
|
+
"messages": messages,
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def get_user(self, validated_token) -> AbstractBaseUser:
|
|
135
|
+
"""
|
|
136
|
+
Returns a stateless user object from the validated token.
|
|
137
|
+
No database lookup - authentication is handled by external service.
|
|
138
|
+
"""
|
|
139
|
+
try:
|
|
140
|
+
user_id = validated_token[api_settings.USER_ID_CLAIM]
|
|
141
|
+
except KeyError as e:
|
|
142
|
+
raise InvalidToken(
|
|
143
|
+
_("Token contained no recognizable user identification")
|
|
144
|
+
) from e
|
|
145
|
+
|
|
146
|
+
# Validate user_id type and value
|
|
147
|
+
if not isinstance(user_id, (int, str)) or not user_id:
|
|
148
|
+
raise InvalidToken(_("Invalid user identification format"))
|
|
149
|
+
|
|
150
|
+
# Additional validation for string user_id
|
|
151
|
+
if isinstance(user_id, str) and len(user_id) > 255:
|
|
152
|
+
raise InvalidToken(_("User identification too long"))
|
|
153
|
+
|
|
154
|
+
# Return stateless TokenUser (no DB lookup)
|
|
155
|
+
return api_settings.TOKEN_USER_CLASS(validated_token)
|
|
156
|
+
|
|
157
|
+
def jwt_authenticate(self, request: HttpRequest, token: str) -> AbstractBaseUser:
|
|
158
|
+
"""
|
|
159
|
+
Authenticate user from JWT token and attach to request.
|
|
160
|
+
"""
|
|
161
|
+
validated_token = self.get_validated_token(token)
|
|
162
|
+
user = self.get_user(validated_token)
|
|
163
|
+
request.user = user
|
|
164
|
+
request.token_user = user # For backward compatibility and request.user can be overridden by other middlewares
|
|
165
|
+
return user
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class JWTHeaderAuthMiddleware(JWTAuthBaseMiddleware):
|
|
169
|
+
"""
|
|
170
|
+
JWT authentication middleware that extracts token from Authorization header.
|
|
171
|
+
Stateless authentication without database lookup.
|
|
172
|
+
"""
|
|
173
|
+
openapi_scheme: str = "bearer"
|
|
174
|
+
header: str = "Authorization"
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def get_token_from_request(self, request: HttpRequest) -> str:
|
|
178
|
+
"""
|
|
179
|
+
Extract JWT token from Authorization header.
|
|
180
|
+
Validates scheme and format without logging sensitive data.
|
|
181
|
+
"""
|
|
182
|
+
headers = request.headers
|
|
183
|
+
auth_value = headers.get(self.header)
|
|
184
|
+
if not auth_value:
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
parts = auth_value.split(" ")
|
|
188
|
+
|
|
189
|
+
# Validate minimum parts
|
|
190
|
+
if len(parts) < 2:
|
|
191
|
+
if settings.DEBUG:
|
|
192
|
+
logger.warning(
|
|
193
|
+
"invalid_authorization_header",
|
|
194
|
+
description="Invalid Authorization header format",
|
|
195
|
+
remote_addr=request.META.get('REMOTE_ADDR')
|
|
196
|
+
)
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
# Validate scheme
|
|
200
|
+
if parts[0].lower() != self.openapi_scheme:
|
|
201
|
+
if settings.DEBUG:
|
|
202
|
+
logger.warning(
|
|
203
|
+
"unexpected_auth_scheme",
|
|
204
|
+
description="Unexpected auth scheme",
|
|
205
|
+
expected_scheme=self.openapi_scheme,
|
|
206
|
+
actual_scheme=parts[0],
|
|
207
|
+
remote_addr=request.META.get('REMOTE_ADDR')
|
|
208
|
+
)
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
token = " ".join(parts[1:])
|
|
212
|
+
|
|
213
|
+
# Basic token format validation (JWT has 3 parts separated by dots)
|
|
214
|
+
if token.count('.') != 2:
|
|
215
|
+
if settings.DEBUG:
|
|
216
|
+
logger.warning(
|
|
217
|
+
"invalid_jwt_format",
|
|
218
|
+
description="Invalid JWT format",
|
|
219
|
+
remote_addr=request.META.get('REMOTE_ADDR')
|
|
220
|
+
)
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
return token
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class JWTCookieAuthMiddleware(JWTAuthBaseMiddleware):
|
|
227
|
+
"""
|
|
228
|
+
JWT authentication middleware that extracts token from cookies.
|
|
229
|
+
Stateless authentication without database lookup.
|
|
230
|
+
"""
|
|
231
|
+
param_name = ACCESS_TOKEN_COOKIE
|
|
232
|
+
|
|
233
|
+
def get_token_from_request(self, request: HttpRequest) -> Optional[str]:
|
|
234
|
+
"""
|
|
235
|
+
Extract JWT token from cookies with CSRF protection.
|
|
236
|
+
|
|
237
|
+
CSRF check is required for cookie-based authentication to prevent
|
|
238
|
+
cross-site request forgery attacks.
|
|
239
|
+
|
|
240
|
+
Override to customize cookie name or CSRF behavior.
|
|
241
|
+
|
|
242
|
+
Raises:
|
|
243
|
+
PermissionDenied: If CSRF check fails
|
|
244
|
+
"""
|
|
245
|
+
# CSRF protection for cookie-based auth
|
|
246
|
+
error_response = check_csrf(request)
|
|
247
|
+
if error_response:
|
|
248
|
+
logger.warning(
|
|
249
|
+
"csrf_check_failed",
|
|
250
|
+
description="CSRF validation failed for cookie-based JWT auth",
|
|
251
|
+
path=request.path,
|
|
252
|
+
remote_addr=request.META.get('REMOTE_ADDR'),
|
|
253
|
+
cookie_name=self.param_name
|
|
254
|
+
)
|
|
255
|
+
raise PermissionDenied("CSRF check failed")
|
|
256
|
+
|
|
257
|
+
return request.COOKIES.get(self.param_name, None)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class BasicNoPasswordAuthMiddleware:
|
|
261
|
+
"""
|
|
262
|
+
DEVELOPMENT ONLY: Basic authentication middleware without password verification.
|
|
263
|
+
|
|
264
|
+
WARNING: This middleware bypasses password authentication and should ONLY be used
|
|
265
|
+
in development environments. It allows authentication by providing only a username/email
|
|
266
|
+
in the Authorization header using Basic auth format.
|
|
267
|
+
|
|
268
|
+
This middleware automatically disables itself when settings.DEBUG is False.
|
|
269
|
+
|
|
270
|
+
Usage:
|
|
271
|
+
Authorization: Basic base64(username:)
|
|
272
|
+
or
|
|
273
|
+
Authorization: Basic base64(username:anything)
|
|
274
|
+
|
|
275
|
+
Example:
|
|
276
|
+
# For user "admin@example.com"
|
|
277
|
+
# Base64 encode: "admin@example.com:"
|
|
278
|
+
# Header: Authorization: Basic YWRtaW5AZXhhbXBsZS5jb206
|
|
279
|
+
|
|
280
|
+
IMPORTANT: Remove this middleware before deploying to production!
|
|
281
|
+
"""
|
|
282
|
+
|
|
283
|
+
header = "Authorization"
|
|
284
|
+
scheme = "basic"
|
|
285
|
+
|
|
286
|
+
def __init__(self, get_response):
|
|
287
|
+
self.get_response = get_response
|
|
288
|
+
self.user_model = get_user_model()
|
|
289
|
+
|
|
290
|
+
# Check if in debug mode - disable completely if not
|
|
291
|
+
self._enabled = settings.DEBUG
|
|
292
|
+
|
|
293
|
+
if self._enabled:
|
|
294
|
+
# Warning log on initialization
|
|
295
|
+
logger.warning(
|
|
296
|
+
"insecure_middleware_loaded",
|
|
297
|
+
description="BasicNoPasswordAuthMiddleware loaded - THIS IS INSECURE AND FOR DEVELOPMENT ONLY",
|
|
298
|
+
middleware=self.__class__.__name__
|
|
299
|
+
)
|
|
300
|
+
else:
|
|
301
|
+
# Log that middleware is disabled
|
|
302
|
+
logger.info(
|
|
303
|
+
"insecure_middleware_disabled",
|
|
304
|
+
description="BasicNoPasswordAuthMiddleware disabled in non-DEBUG mode",
|
|
305
|
+
middleware=self.__class__.__name__
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
def __call__(self, request: HttpRequest):
|
|
309
|
+
"""
|
|
310
|
+
Process the request through the middleware.
|
|
311
|
+
If not in DEBUG mode, simply passes through without any processing.
|
|
312
|
+
"""
|
|
313
|
+
if not self._enabled:
|
|
314
|
+
return self.get_response(request)
|
|
315
|
+
|
|
316
|
+
self.process_request(request)
|
|
317
|
+
return self.get_response(request)
|
|
318
|
+
|
|
319
|
+
def process_request(self, request: HttpRequest):
|
|
320
|
+
"""
|
|
321
|
+
Process request and authenticate user if Basic auth header is present.
|
|
322
|
+
Skips if user is already authenticated.
|
|
323
|
+
Note: This method is only called when DEBUG is True.
|
|
324
|
+
"""
|
|
325
|
+
# Skip if already authenticated
|
|
326
|
+
if hasattr(request, 'user') and request.user.is_authenticated:
|
|
327
|
+
return
|
|
328
|
+
|
|
329
|
+
auth_header = request.headers.get(self.header)
|
|
330
|
+
if not auth_header:
|
|
331
|
+
return
|
|
332
|
+
|
|
333
|
+
try:
|
|
334
|
+
user = self._authenticate(auth_header)
|
|
335
|
+
if user:
|
|
336
|
+
request.user = user
|
|
337
|
+
logger.info(
|
|
338
|
+
"dev_auth_success",
|
|
339
|
+
description="Development authentication successful",
|
|
340
|
+
user_id=user.id,
|
|
341
|
+
username=user.email,
|
|
342
|
+
path=request.path
|
|
343
|
+
)
|
|
344
|
+
except Exception as e:
|
|
345
|
+
logger.debug(
|
|
346
|
+
"dev_auth_failed",
|
|
347
|
+
description="Development authentication failed",
|
|
348
|
+
error=str(e),
|
|
349
|
+
path=request.path
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
def _authenticate(self, auth_header: str) -> Optional[AbstractBaseUser]:
|
|
353
|
+
"""
|
|
354
|
+
Extract credentials from Basic auth header and authenticate user without password.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
auth_header: The Authorization header value
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
User object if found and active, None otherwise
|
|
361
|
+
"""
|
|
362
|
+
parts = auth_header.split(" ")
|
|
363
|
+
|
|
364
|
+
# Validate scheme
|
|
365
|
+
if len(parts) != 2 or parts[0].lower() != self.scheme:
|
|
366
|
+
return None
|
|
367
|
+
|
|
368
|
+
# Decode base64 credentials
|
|
369
|
+
import base64
|
|
370
|
+
try:
|
|
371
|
+
encoded_credentials = parts[1]
|
|
372
|
+
decoded_bytes = base64.b64decode(encoded_credentials)
|
|
373
|
+
decoded_credentials = decoded_bytes.decode('utf-8')
|
|
374
|
+
except Exception:
|
|
375
|
+
logger.debug("Failed to decode base64 credentials")
|
|
376
|
+
return None
|
|
377
|
+
|
|
378
|
+
# Split username:password (password is ignored)
|
|
379
|
+
if ':' in decoded_credentials:
|
|
380
|
+
username, _ = decoded_credentials.split(':', 1)
|
|
381
|
+
else:
|
|
382
|
+
username = decoded_credentials
|
|
383
|
+
|
|
384
|
+
if not username:
|
|
385
|
+
return None
|
|
386
|
+
|
|
387
|
+
# Find user by email or username
|
|
388
|
+
try:
|
|
389
|
+
user = self.user_model.objects.get(email=username)
|
|
390
|
+
except self.user_model.DoesNotExist:
|
|
391
|
+
try:
|
|
392
|
+
# Try by username field if different from email
|
|
393
|
+
user = self.user_model.objects.get(**{self.user_model.USERNAME_FIELD: username})
|
|
394
|
+
except (self.user_model.DoesNotExist, AttributeError):
|
|
395
|
+
logger.debug(f"User not found: {username}")
|
|
396
|
+
return None
|
|
397
|
+
|
|
398
|
+
# Check if user is active
|
|
399
|
+
if not user.is_active:
|
|
400
|
+
logger.debug(f"User {username} is inactive")
|
|
401
|
+
return None
|
|
402
|
+
|
|
403
|
+
return user
|
|
404
|
+
|
oxutils/jwt/models.py
CHANGED
|
@@ -11,23 +11,49 @@ from .tokens import OrganizationAccessToken
|
|
|
11
11
|
logger = structlog.get_logger(__name__)
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
class TenantUser:
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
oxi_id: str | None = None,
|
|
18
|
+
id: str | None = None,
|
|
19
|
+
is_owner: bool = False,
|
|
20
|
+
is_admin: bool = False,
|
|
21
|
+
status: str | None = None,
|
|
22
|
+
):
|
|
23
|
+
self.oxi_id = oxi_id
|
|
24
|
+
self.id = id
|
|
25
|
+
self.is_owner = is_owner
|
|
26
|
+
self.is_admin = is_admin
|
|
27
|
+
self.status = status
|
|
28
|
+
|
|
29
|
+
def __bool__(self):
|
|
30
|
+
return self.status == 'active'
|
|
31
|
+
|
|
32
|
+
def is_active(self):
|
|
33
|
+
return self.status == 'active'
|
|
34
|
+
|
|
35
|
+
|
|
14
36
|
class TokenTenant:
|
|
15
37
|
|
|
16
38
|
def __init__(
|
|
17
39
|
self,
|
|
18
40
|
schema_name: str,
|
|
19
|
-
tenant_id:
|
|
41
|
+
tenant_id: str,
|
|
20
42
|
oxi_id: str,
|
|
21
43
|
subscription_plan: str,
|
|
22
44
|
subscription_status: str,
|
|
23
|
-
|
|
45
|
+
subscription_end_date: str | None = None,
|
|
46
|
+
status: str = 'active',
|
|
47
|
+
user: TenantUser | None = None,
|
|
24
48
|
):
|
|
25
49
|
self.schema_name = schema_name
|
|
26
50
|
self.id = tenant_id
|
|
27
51
|
self.oxi_id = oxi_id
|
|
28
52
|
self.subscription_plan = subscription_plan
|
|
29
53
|
self.subscription_status = subscription_status
|
|
54
|
+
self.subscription_end_date = subscription_end_date
|
|
30
55
|
self.status = status
|
|
56
|
+
self.user = user
|
|
31
57
|
|
|
32
58
|
def __str__(self):
|
|
33
59
|
return f"{self.schema_name} - {self.oxi_id}"
|
|
@@ -44,23 +70,93 @@ class TokenTenant:
|
|
|
44
70
|
def is_deleted(self):
|
|
45
71
|
return self.status == 'deleted'
|
|
46
72
|
|
|
73
|
+
def __bool__(self):
|
|
74
|
+
return self.status == 'active'
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def is_admin_user(self):
|
|
78
|
+
if self.user:
|
|
79
|
+
return self.user.is_admin
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def is_owner_user(self):
|
|
84
|
+
if self.user:
|
|
85
|
+
return self.user.is_owner
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def is_tenant_user(self):
|
|
90
|
+
if self.user:
|
|
91
|
+
return self.user.is_active()
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
def get_tenant_type(self):
|
|
95
|
+
return self.subscription_status
|
|
96
|
+
|
|
47
97
|
@classmethod
|
|
48
98
|
def for_token(cls, token):
|
|
49
99
|
try:
|
|
50
100
|
token_obj = OrganizationAccessToken(token=token)
|
|
101
|
+
|
|
102
|
+
# set the tenant user
|
|
103
|
+
if 'tenant_user_id' in token_obj and token_obj.get('tenant_user_id'):
|
|
104
|
+
user = TenantUser(
|
|
105
|
+
oxi_id=token_obj.get('tenant_user_oxi_id'),
|
|
106
|
+
id=token_obj.get('tenant_user_id'),
|
|
107
|
+
is_owner=token_obj.get('tenant_user_is_owner'),
|
|
108
|
+
is_admin=token_obj.get('tenant_user_is_admin'),
|
|
109
|
+
status=token_obj.get('tenant_user_status'),
|
|
110
|
+
)
|
|
111
|
+
else:
|
|
112
|
+
user = TenantUser()
|
|
113
|
+
|
|
51
114
|
tenant = cls(
|
|
52
115
|
schema_name=token_obj['schema_name'],
|
|
53
116
|
tenant_id=token_obj['tenant_id'],
|
|
54
117
|
oxi_id=token_obj['oxi_id'],
|
|
55
118
|
subscription_plan=token_obj['subscription_plan'],
|
|
56
119
|
subscription_status=token_obj['subscription_status'],
|
|
120
|
+
subscription_end_date=token_obj.get('subscription_end_date'),
|
|
57
121
|
status=token_obj['status'],
|
|
122
|
+
user=user,
|
|
58
123
|
)
|
|
124
|
+
|
|
59
125
|
return tenant
|
|
60
126
|
except Exception:
|
|
61
127
|
logger.exception('Failed to create TokenTenant from token', token=token)
|
|
62
128
|
return None
|
|
63
129
|
|
|
130
|
+
@classmethod
|
|
131
|
+
def from_db(cls, tenant) -> 'TokenTenant':
|
|
132
|
+
if not tenant:
|
|
133
|
+
raise ValueError('Tenant is required')
|
|
134
|
+
|
|
135
|
+
if hasattr(tenant, 'user') and tenant.user:
|
|
136
|
+
user = TenantUser(
|
|
137
|
+
oxi_id=tenant.user.user.oxi_id,
|
|
138
|
+
id=tenant.user.id,
|
|
139
|
+
is_owner=tenant.user.is_owner,
|
|
140
|
+
is_admin=tenant.user.is_admin,
|
|
141
|
+
status=tenant.user.status,
|
|
142
|
+
)
|
|
143
|
+
else:
|
|
144
|
+
user = TenantUser()
|
|
145
|
+
|
|
146
|
+
return cls(
|
|
147
|
+
schema_name=tenant.schema_name,
|
|
148
|
+
tenant_id=tenant.id,
|
|
149
|
+
oxi_id=tenant.oxi_id,
|
|
150
|
+
subscription_plan=tenant.subscription_plan,
|
|
151
|
+
subscription_status=tenant.subscription_status,
|
|
152
|
+
subscription_end_date=tenant.subscription_end_date,
|
|
153
|
+
status=tenant.status,
|
|
154
|
+
user=user,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def __repr__(self):
|
|
158
|
+
return f"TokenTenant(schema_name='{self.schema_name}', oxi_id='{self.oxi_id}', status='{self.status}')"
|
|
159
|
+
|
|
64
160
|
|
|
65
161
|
class TokenUser(DefaultTonkenUser):
|
|
66
162
|
@cached_property
|
oxutils/jwt/tokens.py
CHANGED
|
@@ -58,6 +58,14 @@ class OrganizationAccessToken(Token):
|
|
|
58
58
|
token.payload['subscription_end_date'] = str(tenant.subscription_end_date)
|
|
59
59
|
token.payload['status'] = str(tenant.status)
|
|
60
60
|
|
|
61
|
+
# Add tenant user info if available
|
|
62
|
+
if hasattr(tenant, 'user'):
|
|
63
|
+
token.payload['tenant_user_oxi_id'] = str(tenant.user.user.oxi_id)
|
|
64
|
+
token.payload['tenant_user_id'] = str(tenant.user.id)
|
|
65
|
+
token.payload['tenant_user_is_owner'] = tenant.user.is_owner
|
|
66
|
+
token.payload['tenant_user_is_admin'] = tenant.user.is_admin
|
|
67
|
+
token.payload['tenant_user_status'] = str(tenant.user.status)
|
|
68
|
+
|
|
61
69
|
return token
|
|
62
70
|
|
|
63
71
|
@property
|