django-bolt 0.1.0__cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.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.

Potentially problematic release.


This version of django-bolt might be problematic. Click here for more details.

Files changed (128) hide show
  1. django_bolt/__init__.py +147 -0
  2. django_bolt/_core.abi3.so +0 -0
  3. django_bolt/admin/__init__.py +25 -0
  4. django_bolt/admin/admin_detection.py +179 -0
  5. django_bolt/admin/asgi_bridge.py +267 -0
  6. django_bolt/admin/routes.py +91 -0
  7. django_bolt/admin/static.py +155 -0
  8. django_bolt/admin/static_routes.py +111 -0
  9. django_bolt/api.py +1011 -0
  10. django_bolt/apps.py +7 -0
  11. django_bolt/async_collector.py +228 -0
  12. django_bolt/auth/README.md +464 -0
  13. django_bolt/auth/REVOCATION_EXAMPLE.md +391 -0
  14. django_bolt/auth/__init__.py +84 -0
  15. django_bolt/auth/backends.py +236 -0
  16. django_bolt/auth/guards.py +224 -0
  17. django_bolt/auth/jwt_utils.py +212 -0
  18. django_bolt/auth/revocation.py +286 -0
  19. django_bolt/auth/token.py +335 -0
  20. django_bolt/binding.py +363 -0
  21. django_bolt/bootstrap.py +77 -0
  22. django_bolt/cli.py +133 -0
  23. django_bolt/compression.py +104 -0
  24. django_bolt/decorators.py +159 -0
  25. django_bolt/dependencies.py +128 -0
  26. django_bolt/error_handlers.py +305 -0
  27. django_bolt/exceptions.py +294 -0
  28. django_bolt/health.py +129 -0
  29. django_bolt/logging/__init__.py +6 -0
  30. django_bolt/logging/config.py +357 -0
  31. django_bolt/logging/middleware.py +296 -0
  32. django_bolt/management/__init__.py +1 -0
  33. django_bolt/management/commands/__init__.py +0 -0
  34. django_bolt/management/commands/runbolt.py +427 -0
  35. django_bolt/middleware/__init__.py +32 -0
  36. django_bolt/middleware/compiler.py +131 -0
  37. django_bolt/middleware/middleware.py +247 -0
  38. django_bolt/openapi/__init__.py +23 -0
  39. django_bolt/openapi/config.py +196 -0
  40. django_bolt/openapi/plugins.py +439 -0
  41. django_bolt/openapi/routes.py +152 -0
  42. django_bolt/openapi/schema_generator.py +581 -0
  43. django_bolt/openapi/spec/__init__.py +68 -0
  44. django_bolt/openapi/spec/base.py +74 -0
  45. django_bolt/openapi/spec/callback.py +24 -0
  46. django_bolt/openapi/spec/components.py +72 -0
  47. django_bolt/openapi/spec/contact.py +21 -0
  48. django_bolt/openapi/spec/discriminator.py +25 -0
  49. django_bolt/openapi/spec/encoding.py +67 -0
  50. django_bolt/openapi/spec/enums.py +41 -0
  51. django_bolt/openapi/spec/example.py +36 -0
  52. django_bolt/openapi/spec/external_documentation.py +21 -0
  53. django_bolt/openapi/spec/header.py +132 -0
  54. django_bolt/openapi/spec/info.py +50 -0
  55. django_bolt/openapi/spec/license.py +28 -0
  56. django_bolt/openapi/spec/link.py +66 -0
  57. django_bolt/openapi/spec/media_type.py +51 -0
  58. django_bolt/openapi/spec/oauth_flow.py +36 -0
  59. django_bolt/openapi/spec/oauth_flows.py +28 -0
  60. django_bolt/openapi/spec/open_api.py +87 -0
  61. django_bolt/openapi/spec/operation.py +105 -0
  62. django_bolt/openapi/spec/parameter.py +147 -0
  63. django_bolt/openapi/spec/path_item.py +78 -0
  64. django_bolt/openapi/spec/paths.py +27 -0
  65. django_bolt/openapi/spec/reference.py +38 -0
  66. django_bolt/openapi/spec/request_body.py +38 -0
  67. django_bolt/openapi/spec/response.py +48 -0
  68. django_bolt/openapi/spec/responses.py +44 -0
  69. django_bolt/openapi/spec/schema.py +678 -0
  70. django_bolt/openapi/spec/security_requirement.py +28 -0
  71. django_bolt/openapi/spec/security_scheme.py +69 -0
  72. django_bolt/openapi/spec/server.py +34 -0
  73. django_bolt/openapi/spec/server_variable.py +32 -0
  74. django_bolt/openapi/spec/tag.py +32 -0
  75. django_bolt/openapi/spec/xml.py +44 -0
  76. django_bolt/pagination.py +669 -0
  77. django_bolt/param_functions.py +49 -0
  78. django_bolt/params.py +337 -0
  79. django_bolt/request_parsing.py +128 -0
  80. django_bolt/responses.py +214 -0
  81. django_bolt/router.py +48 -0
  82. django_bolt/serialization.py +193 -0
  83. django_bolt/status_codes.py +321 -0
  84. django_bolt/testing/__init__.py +10 -0
  85. django_bolt/testing/client.py +274 -0
  86. django_bolt/testing/helpers.py +93 -0
  87. django_bolt/tests/__init__.py +0 -0
  88. django_bolt/tests/admin_tests/__init__.py +1 -0
  89. django_bolt/tests/admin_tests/conftest.py +6 -0
  90. django_bolt/tests/admin_tests/test_admin_with_django.py +278 -0
  91. django_bolt/tests/admin_tests/urls.py +9 -0
  92. django_bolt/tests/cbv/__init__.py +0 -0
  93. django_bolt/tests/cbv/test_class_views.py +570 -0
  94. django_bolt/tests/cbv/test_class_views_django_orm.py +703 -0
  95. django_bolt/tests/cbv/test_class_views_features.py +1173 -0
  96. django_bolt/tests/cbv/test_class_views_with_client.py +622 -0
  97. django_bolt/tests/conftest.py +165 -0
  98. django_bolt/tests/test_action_decorator.py +399 -0
  99. django_bolt/tests/test_auth_secret_key.py +83 -0
  100. django_bolt/tests/test_decorator_syntax.py +159 -0
  101. django_bolt/tests/test_error_handling.py +481 -0
  102. django_bolt/tests/test_file_response.py +192 -0
  103. django_bolt/tests/test_global_cors.py +172 -0
  104. django_bolt/tests/test_guards_auth.py +441 -0
  105. django_bolt/tests/test_guards_integration.py +303 -0
  106. django_bolt/tests/test_health.py +283 -0
  107. django_bolt/tests/test_integration_validation.py +400 -0
  108. django_bolt/tests/test_json_validation.py +536 -0
  109. django_bolt/tests/test_jwt_auth.py +327 -0
  110. django_bolt/tests/test_jwt_token.py +458 -0
  111. django_bolt/tests/test_logging.py +837 -0
  112. django_bolt/tests/test_logging_merge.py +419 -0
  113. django_bolt/tests/test_middleware.py +492 -0
  114. django_bolt/tests/test_middleware_server.py +230 -0
  115. django_bolt/tests/test_model_viewset.py +323 -0
  116. django_bolt/tests/test_models.py +24 -0
  117. django_bolt/tests/test_pagination.py +1258 -0
  118. django_bolt/tests/test_parameter_validation.py +178 -0
  119. django_bolt/tests/test_syntax.py +626 -0
  120. django_bolt/tests/test_testing_utilities.py +163 -0
  121. django_bolt/tests/test_testing_utilities_simple.py +123 -0
  122. django_bolt/tests/test_viewset_unified.py +346 -0
  123. django_bolt/typing.py +273 -0
  124. django_bolt/views.py +1110 -0
  125. django_bolt-0.1.0.dist-info/METADATA +629 -0
  126. django_bolt-0.1.0.dist-info/RECORD +128 -0
  127. django_bolt-0.1.0.dist-info/WHEEL +4 -0
  128. django_bolt-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,391 @@
1
+ # Token Revocation Examples
2
+
3
+ Token revocation is **OPTIONAL** in Django-Bolt. Only use it if you need logout functionality or token invalidation.
4
+
5
+ ## Quick Start
6
+
7
+ ### Option 1: In-Memory (Development/Single-Process)
8
+
9
+ ```python
10
+ from django_bolt import BoltAPI, JWTAuthentication, Token, IsAuthenticated
11
+ from django_bolt.auth.revocation import InMemoryRevocation
12
+ from datetime import timedelta
13
+ import uuid
14
+
15
+ # Create revocation store
16
+ revocation = InMemoryRevocation()
17
+
18
+ # Create API with revocation-enabled auth
19
+ api = BoltAPI()
20
+
21
+ auth = JWTAuthentication(
22
+ secret=settings.SECRET_KEY,
23
+ revocation_store=revocation, # ← OPTIONAL: Enables revocation
24
+ require_jti=True, # ← Auto-enabled when revocation_store is provided
25
+ )
26
+
27
+ # Login endpoint - create tokens with JTI
28
+ @api.post("/login")
29
+ async def login(username: str, password: str):
30
+ user = await authenticate(username=username, password=password)
31
+
32
+ if not user:
33
+ return {"error": "Invalid credentials"}, 401
34
+
35
+ # Create token with JTI (required for revocation)
36
+ token = Token.create(
37
+ sub=str(user.id),
38
+ expires_delta=timedelta(hours=1),
39
+ jti=str(uuid.uuid4()), # ← Unique token ID (required for revocation)
40
+ is_staff=user.is_staff,
41
+ is_admin=user.is_superuser,
42
+ )
43
+
44
+ return {
45
+ "access_token": token.encode(settings.SECRET_KEY),
46
+ "token_type": "bearer"
47
+ }
48
+
49
+ # Logout endpoint - revoke token
50
+ @api.post("/logout", auth=[auth], guards=[IsAuthenticated()])
51
+ async def logout(request):
52
+ jti = request["context"]["auth_claims"]["jti"]
53
+
54
+ # Revoke the token
55
+ await revocation.revoke(jti, ttl=3600) # TTL = remaining token lifetime
56
+
57
+ return {"message": "Logged out successfully"}
58
+
59
+ # Protected endpoint
60
+ @api.get("/profile", auth=[auth], guards=[IsAuthenticated()])
61
+ async def get_profile(request):
62
+ user_id = request["context"]["user_id"]
63
+ return {"user_id": user_id}
64
+ ```
65
+
66
+ ---
67
+
68
+ ### Option 2: Django Cache (Production - Redis/Memcached)
69
+
70
+ ```python
71
+ from django_bolt import BoltAPI, JWTAuthentication
72
+ from django_bolt.auth.revocation import DjangoCacheRevocation
73
+
74
+ # settings.py
75
+ CACHES = {
76
+ 'default': {
77
+ 'BACKEND': 'django.core.cache.backends.redis.RedisCache',
78
+ 'LOCATION': 'redis://127.0.0.1:6379/1',
79
+ }
80
+ }
81
+
82
+ # api.py
83
+ revocation = DjangoCacheRevocation(
84
+ cache_alias='default',
85
+ key_prefix='revoked:', # Keys will be "revoked:{jti}"
86
+ )
87
+
88
+ auth = JWTAuthentication(
89
+ secret=settings.SECRET_KEY,
90
+ revocation_store=revocation,
91
+ )
92
+
93
+ @api.post("/logout", auth=[auth])
94
+ async def logout(request):
95
+ jti = request["context"]["auth_claims"]["jti"]
96
+
97
+ # Revoke token - stored in Redis with TTL
98
+ await revocation.revoke(jti, ttl=86400 * 30) # 30 days
99
+
100
+ return {"message": "Token revoked"}
101
+ ```
102
+
103
+ ---
104
+
105
+ ### Option 3: Database (No Cache Infrastructure)
106
+
107
+ ```python
108
+ # myapp/models.py
109
+ from django.db import models
110
+
111
+ class RevokedToken(models.Model):
112
+ jti = models.CharField(max_length=255, unique=True, db_index=True)
113
+ revoked_at = models.DateTimeField(auto_now_add=True)
114
+ expires_at = models.DateTimeField(db_index=True)
115
+
116
+ class Meta:
117
+ indexes = [
118
+ models.Index(fields=['jti']),
119
+ models.Index(fields=['expires_at']),
120
+ ]
121
+ ```
122
+
123
+ ```python
124
+ # api.py
125
+ from django_bolt.auth.revocation import DjangoORMRevocation
126
+
127
+ revocation = DjangoORMRevocation(model='myapp.RevokedToken')
128
+
129
+ auth = JWTAuthentication(
130
+ secret=settings.SECRET_KEY,
131
+ revocation_store=revocation,
132
+ )
133
+
134
+ @api.post("/logout", auth=[auth])
135
+ async def logout(request):
136
+ jti = request["context"]["auth_claims"]["jti"]
137
+
138
+ # Revoke token - stored in database
139
+ await revocation.revoke(jti, ttl=86400 * 30)
140
+
141
+ return {"message": "Token revoked"}
142
+ ```
143
+
144
+ **⚠️ Important**: Add cleanup task for expired tokens:
145
+
146
+ ```python
147
+ # Celery task or cron job
148
+ from datetime import datetime, timezone
149
+ from myapp.models import RevokedToken
150
+
151
+ async def cleanup_expired_tokens():
152
+ """Run this periodically (e.g., daily)"""
153
+ await RevokedToken.objects.filter(
154
+ expires_at__lt=datetime.now(timezone.utc)
155
+ ).adelete()
156
+ ```
157
+
158
+ ---
159
+
160
+ ### Option 4: Custom Revocation Handler
161
+
162
+ If you have custom logic or use a different storage backend:
163
+
164
+ ```python
165
+ from django_bolt import JWTAuthentication
166
+ import httpx
167
+
168
+ # Custom handler - checks external service
169
+ async def check_token_revoked(jti: str) -> bool:
170
+ """Custom revocation logic"""
171
+ async with httpx.AsyncClient() as client:
172
+ response = await client.get(f"https://auth-service/check-revoked/{jti}")
173
+ return response.json()["revoked"]
174
+
175
+ auth = JWTAuthentication(
176
+ secret=settings.SECRET_KEY,
177
+ revoked_token_handler=check_token_revoked, # ← Custom function
178
+ require_jti=True,
179
+ )
180
+ ```
181
+
182
+ ---
183
+
184
+ ## Without Revocation (Simpler, Faster)
185
+
186
+ If you don't need logout functionality, just don't provide a revocation handler:
187
+
188
+ ```python
189
+ # NO revocation - tokens valid until expiry
190
+ auth = JWTAuthentication(
191
+ secret=settings.SECRET_KEY,
192
+ # No revocation_store or revoked_token_handler
193
+ )
194
+
195
+ # Users can't logout - tokens expire naturally
196
+ # ✅ Simpler
197
+ # ✅ Faster (~60k RPS vs ~50k RPS with revocation)
198
+ # ❌ No logout support
199
+ ```
200
+
201
+ ---
202
+
203
+ ## Performance Comparison
204
+
205
+ | Revocation Strategy | RPS | Multi-Process | Notes |
206
+ |---------------------|-----|---------------|-------|
207
+ | **No Revocation** | ~60k | ✅ | Fastest, no logout |
208
+ | **InMemoryRevocation** | ~58k | ❌ | Single process only |
209
+ | **DjangoCacheRevocation (Redis)** | ~50k | ✅ | **Recommended for production** |
210
+ | **DjangoCacheRevocation (Memcached)** | ~48k | ✅ | Good alternative |
211
+ | **DjangoORMRevocation (PostgreSQL)** | ~5k | ✅ | Slowest, use if no cache |
212
+ | **DjangoORMRevocation (SQLite)** | ~2k | ⚠️ | Not for production |
213
+
214
+ ---
215
+
216
+ ## Complete Login/Logout Example
217
+
218
+ ```python
219
+ from django_bolt import BoltAPI, JWTAuthentication, Token, IsAuthenticated, AllowAny
220
+ from django_bolt.auth.revocation import DjangoCacheRevocation
221
+ from django.contrib.auth import authenticate
222
+ from datetime import timedelta
223
+ import uuid
224
+
225
+ api = BoltAPI()
226
+
227
+ # Setup revocation
228
+ revocation = DjangoCacheRevocation()
229
+
230
+ auth = JWTAuthentication(
231
+ secret=settings.SECRET_KEY,
232
+ revocation_store=revocation,
233
+ )
234
+
235
+ # Public login endpoint
236
+ @api.post("/auth/login", guards=[AllowAny()])
237
+ async def login(username: str, password: str):
238
+ """Authenticate and return JWT token."""
239
+ user = await authenticate(username=username, password=password)
240
+
241
+ if not user:
242
+ return {"error": "Invalid credentials"}, 401
243
+
244
+ # Create access token (short-lived)
245
+ access_token = Token.create(
246
+ sub=str(user.id),
247
+ expires_delta=timedelta(minutes=15),
248
+ jti=str(uuid.uuid4()), # Required for revocation
249
+ is_staff=user.is_staff,
250
+ is_admin=user.is_superuser,
251
+ permissions=list(user.get_all_permissions()),
252
+ extras={
253
+ "username": user.username,
254
+ "email": user.email,
255
+ }
256
+ )
257
+
258
+ # Optional: Create refresh token (long-lived)
259
+ refresh_token = Token.create(
260
+ sub=str(user.id),
261
+ expires_delta=timedelta(days=30),
262
+ jti=str(uuid.uuid4()),
263
+ extras={"type": "refresh"}
264
+ )
265
+
266
+ return {
267
+ "access_token": access_token.encode(settings.SECRET_KEY),
268
+ "refresh_token": refresh_token.encode(settings.SECRET_KEY),
269
+ "token_type": "bearer",
270
+ "expires_in": 900 # 15 minutes
271
+ }
272
+
273
+ # Protected logout endpoint
274
+ @api.post("/auth/logout", auth=[auth], guards=[IsAuthenticated()])
275
+ async def logout(request):
276
+ """Logout - revoke current token."""
277
+ jti = request["context"]["auth_claims"]["jti"]
278
+
279
+ # Calculate TTL from token expiry
280
+ exp = request["context"]["auth_claims"]["exp"]
281
+ import time
282
+ ttl = max(0, exp - int(time.time()))
283
+
284
+ # Revoke token
285
+ await revocation.revoke(jti, ttl=ttl)
286
+
287
+ return {"message": "Logged out successfully"}
288
+
289
+ # Refresh token endpoint
290
+ @api.post("/auth/refresh", guards=[AllowAny()])
291
+ async def refresh_access_token(refresh_token: str):
292
+ """Exchange refresh token for new access token."""
293
+ try:
294
+ # Decode refresh token
295
+ token = Token.decode(refresh_token, secret=settings.SECRET_KEY)
296
+
297
+ # Validate it's a refresh token
298
+ if token.extras.get("type") != "refresh":
299
+ return {"error": "Invalid token type"}, 401
300
+
301
+ # Check if revoked
302
+ if await revocation.is_revoked(token.jti):
303
+ return {"error": "Token has been revoked"}, 401
304
+
305
+ # Create new access token
306
+ new_access = Token.create(
307
+ sub=token.sub,
308
+ expires_delta=timedelta(minutes=15),
309
+ jti=str(uuid.uuid4()),
310
+ )
311
+
312
+ return {
313
+ "access_token": new_access.encode(settings.SECRET_KEY),
314
+ "token_type": "bearer",
315
+ "expires_in": 900
316
+ }
317
+
318
+ except ValueError as e:
319
+ return {"error": "Invalid token"}, 401
320
+
321
+ # Protected endpoint
322
+ @api.get("/profile", auth=[auth], guards=[IsAuthenticated()])
323
+ async def get_profile(request):
324
+ """Get current user profile."""
325
+ user_id = request["context"]["user_id"]
326
+ is_admin = request["context"]["is_admin"]
327
+ permissions = request["context"].get("permissions", [])
328
+
329
+ return {
330
+ "user_id": user_id,
331
+ "is_admin": is_admin,
332
+ "permissions": permissions,
333
+ }
334
+ ```
335
+
336
+ ---
337
+
338
+ ## Best Practices
339
+
340
+ ### 1. Always Use JTI with Revocation
341
+
342
+ ```python
343
+ # ✅ Good - JTI for revocation
344
+ token = Token.create(
345
+ sub="user123",
346
+ jti=str(uuid.uuid4()), # Unique ID
347
+ expires_delta=timedelta(hours=1)
348
+ )
349
+
350
+ # ❌ Bad - No JTI, can't revoke
351
+ token = Token.create(
352
+ sub="user123",
353
+ expires_delta=timedelta(hours=1)
354
+ )
355
+ ```
356
+
357
+ ### 2. Set Appropriate TTL
358
+
359
+ ```python
360
+ # Match revocation TTL to token expiry
361
+ token = Token.create(sub="user123", expires_delta=timedelta(hours=1))
362
+
363
+ # Revoke with same TTL (3600 seconds = 1 hour)
364
+ await revocation.revoke(jti, ttl=3600)
365
+
366
+ # After 1 hour, token expires AND revocation entry is cleaned up
367
+ ```
368
+
369
+ ### 3. Use DjangoCacheRevocation in Production
370
+
371
+ ```python
372
+ # ✅ Recommended for production
373
+ revocation = DjangoCacheRevocation() # Uses Redis/Memcached
374
+
375
+ # ⚠️ Only for development
376
+ revocation = InMemoryRevocation() # Single-process only
377
+
378
+ # 🐌 Slower but works without cache
379
+ revocation = DjangoORMRevocation(model='myapp.RevokedToken')
380
+ ```
381
+
382
+ ---
383
+
384
+ ## Summary
385
+
386
+ - **Revocation is OPTIONAL** - only use if you need logout
387
+ - **Multiple storage options** - In-memory, Cache, Database, Custom
388
+ - **No Redis requirement** - Works with Django cache (any backend)
389
+ - **Performance-aware** - Cache-based revocation still does ~50k RPS
390
+ - **JTI auto-required** - When revocation is enabled, JTI becomes mandatory
391
+ - **Flexible** - Bring your own storage with custom handler
@@ -0,0 +1,84 @@
1
+ """
2
+ Django-Bolt Authentication and Authorization System.
3
+
4
+ High-performance auth system where validation happens in Rust without the GIL.
5
+ Python classes define configuration that gets compiled to Rust types.
6
+ """
7
+
8
+ # Authentication backends
9
+ from .backends import (
10
+ BaseAuthentication,
11
+ JWTAuthentication,
12
+ APIKeyAuthentication,
13
+ SessionAuthentication,
14
+ AuthContext,
15
+ get_default_authentication_classes,
16
+ )
17
+
18
+ # Permission guards
19
+ from .guards import (
20
+ BasePermission,
21
+ AllowAny,
22
+ IsAuthenticated,
23
+ IsAdminUser,
24
+ IsStaff,
25
+ HasPermission,
26
+ HasAnyPermission,
27
+ HasAllPermissions,
28
+ get_default_permission_classes,
29
+ )
30
+
31
+ # JWT Token handling
32
+ from .token import Token
33
+
34
+ # JWT utilities for Django User integration
35
+ from .jwt_utils import (
36
+ create_jwt_for_user,
37
+ get_current_user,
38
+ extract_user_id_from_context,
39
+ get_auth_context,
40
+ )
41
+
42
+ # Token revocation (optional)
43
+ from .revocation import (
44
+ RevocationStore,
45
+ InMemoryRevocation,
46
+ DjangoCacheRevocation,
47
+ DjangoORMRevocation,
48
+ create_revocation_handler,
49
+ )
50
+
51
+ __all__ = [
52
+ # Authentication
53
+ "BaseAuthentication",
54
+ "JWTAuthentication",
55
+ "APIKeyAuthentication",
56
+ "SessionAuthentication",
57
+ "AuthContext",
58
+ "get_default_authentication_classes",
59
+
60
+ # Guards/Permissions
61
+ "BasePermission",
62
+ "AllowAny",
63
+ "IsAuthenticated",
64
+ "IsAdminUser",
65
+ "IsStaff",
66
+ "HasPermission",
67
+ "HasAnyPermission",
68
+ "HasAllPermissions",
69
+ "get_default_permission_classes",
70
+
71
+ # JWT
72
+ "Token",
73
+ "create_jwt_for_user",
74
+ "get_current_user",
75
+ "extract_user_id_from_context",
76
+ "get_auth_context",
77
+
78
+ # Revocation (optional)
79
+ "RevocationStore",
80
+ "InMemoryRevocation",
81
+ "DjangoCacheRevocation",
82
+ "DjangoORMRevocation",
83
+ "create_revocation_handler",
84
+ ]
@@ -0,0 +1,236 @@
1
+ """
2
+ Authentication system for Django-Bolt.
3
+
4
+ Provides DRF-inspired authentication classes that are compiled to Rust types
5
+ for zero-GIL performance in the hot path.
6
+
7
+ The authentication flow:
8
+ 1. Python defines auth backends (JWT, API key, session)
9
+ 2. Backends compile to metadata dicts via to_metadata()
10
+ 3. Rust parses metadata at registration time
11
+ 4. Rust validates tokens/keys without GIL on each request
12
+ 5. AuthContext is populated and passed to Python handlers
13
+
14
+ Performance: ~60k+ RPS with JWT validation happening entirely in Rust.
15
+ """
16
+
17
+ from abc import ABC, abstractmethod
18
+ from typing import Any, Dict, List, Optional, Set
19
+ from dataclasses import dataclass
20
+
21
+
22
+ @dataclass
23
+ class AuthContext:
24
+ """
25
+ Authentication context returned by authentication backends.
26
+
27
+ This is populated in Rust and passed to Python handlers via request.context.
28
+ """
29
+ user_id: Optional[str] = None
30
+ is_staff: bool = False
31
+ is_admin: bool = False
32
+ backend: str = "none"
33
+ claims: Optional[Dict[str, Any]] = None
34
+ permissions: Optional[Set[str]] = None
35
+
36
+
37
+ class BaseAuthentication(ABC):
38
+ """
39
+ Base class for authentication backends.
40
+
41
+ Authentication happens in Rust for performance. These classes compile
42
+ their configuration into metadata that Rust uses to validate tokens/keys.
43
+ """
44
+
45
+ @property
46
+ @abstractmethod
47
+ def scheme_name(self) -> str:
48
+ """Return the authentication scheme name (e.g., 'jwt', 'api_key')"""
49
+ pass
50
+
51
+ @abstractmethod
52
+ def to_metadata(self) -> Dict[str, Any]:
53
+ """
54
+ Compile this authentication backend into metadata for Rust.
55
+
56
+ Returns a dict that will be parsed by Rust into typed enums.
57
+ """
58
+ pass
59
+
60
+
61
+ class JWTAuthentication(BaseAuthentication):
62
+ """
63
+ JWT token authentication.
64
+
65
+ Validates JWT tokens using the configured secret and algorithms.
66
+ Tokens should be provided in the Authorization header as "Bearer <token>".
67
+
68
+ Args:
69
+ secret: Secret key for JWT validation. If None, uses Django's SECRET_KEY.
70
+ algorithms: List of allowed JWT algorithms (default: ["HS256"])
71
+ header: Header name to extract token from (default: "authorization")
72
+ audience: Optional JWT audience claim to validate
73
+ issuer: Optional JWT issuer claim to validate
74
+ """
75
+
76
+ def __init__(
77
+ self,
78
+ secret: Optional[str] = None,
79
+ algorithms: Optional[List[str]] = None,
80
+ header: str = "authorization",
81
+ audience: Optional[str] = None,
82
+ issuer: Optional[str] = None,
83
+ revoked_token_handler: Optional[callable] = None,
84
+ revocation_store: Optional[Any] = None,
85
+ require_jti: bool = False,
86
+ ):
87
+ self.secret = secret
88
+ self.algorithms = algorithms or ["HS256"]
89
+ self.header = header
90
+ self.audience = audience
91
+ self.issuer = issuer
92
+
93
+ # If no secret provided, try to get Django's SECRET_KEY
94
+ if self.secret is None:
95
+ try:
96
+ from django.conf import settings
97
+ from django.core.exceptions import ImproperlyConfigured
98
+
99
+ if not hasattr(settings, 'SECRET_KEY'):
100
+ raise ImproperlyConfigured(
101
+ "JWTAuthentication requires a 'secret' parameter or Django's SECRET_KEY setting. "
102
+ "Neither was provided."
103
+ )
104
+
105
+ self.secret = settings.SECRET_KEY
106
+
107
+ if not self.secret or self.secret == '':
108
+ raise ImproperlyConfigured(
109
+ "JWTAuthentication secret cannot be empty. "
110
+ "Please provide a non-empty 'secret' parameter or set Django's SECRET_KEY."
111
+ )
112
+ except ImportError:
113
+ from django.core.exceptions import ImproperlyConfigured
114
+ raise ImproperlyConfigured(
115
+ "JWTAuthentication requires Django to be installed and configured, "
116
+ "or a 'secret' parameter must be explicitly provided."
117
+ )
118
+
119
+ # Revocation support (OPTIONAL - only checked if provided)
120
+ self.revoked_token_handler = revoked_token_handler
121
+ self.revocation_store = revocation_store
122
+
123
+ # Auto-enable require_jti if revocation is configured
124
+ if (revoked_token_handler or revocation_store) and not require_jti:
125
+ require_jti = True
126
+ self.require_jti = require_jti
127
+
128
+ # If revocation_store provided, create handler from it
129
+ if revocation_store and not revoked_token_handler:
130
+ from .revocation import create_revocation_handler
131
+ self.revoked_token_handler = create_revocation_handler(revocation_store)
132
+
133
+ @property
134
+ def scheme_name(self) -> str:
135
+ return "jwt"
136
+
137
+ def to_metadata(self) -> Dict[str, Any]:
138
+ metadata = {
139
+ "type": "jwt",
140
+ "secret": self.secret,
141
+ "algorithms": self.algorithms,
142
+ "header": self.header.lower(),
143
+ "audience": self.audience,
144
+ "issuer": self.issuer,
145
+ "require_jti": self.require_jti,
146
+ }
147
+
148
+ # Add revocation handler reference (will be called from Rust if present)
149
+ if self.revoked_token_handler:
150
+ metadata["has_revocation_handler"] = True
151
+
152
+ return metadata
153
+
154
+
155
+ class APIKeyAuthentication(BaseAuthentication):
156
+ """
157
+ API key authentication.
158
+
159
+ Validates API keys against a configured set of valid keys.
160
+ Keys should be provided in the configured header (default: X-API-Key).
161
+
162
+ Args:
163
+ api_keys: Set of valid API keys
164
+ header: Header name to extract API key from (default: "x-api-key")
165
+ key_permissions: Optional mapping of API keys to permission sets
166
+ """
167
+
168
+ def __init__(
169
+ self,
170
+ api_keys: Optional[Set[str]] = None,
171
+ header: str = "x-api-key",
172
+ key_permissions: Optional[Dict[str, Set[str]]] = None,
173
+ ):
174
+ self.api_keys = api_keys or set()
175
+ self.header = header
176
+ self.key_permissions = key_permissions or {}
177
+
178
+ @property
179
+ def scheme_name(self) -> str:
180
+ return "api_key"
181
+
182
+ def to_metadata(self) -> Dict[str, Any]:
183
+ return {
184
+ "type": "api_key",
185
+ "api_keys": list(self.api_keys),
186
+ "header": self.header.lower(),
187
+ "key_permissions": {
188
+ k: list(v) for k, v in self.key_permissions.items()
189
+ },
190
+ }
191
+
192
+
193
+ class SessionAuthentication(BaseAuthentication):
194
+ """
195
+ Django session authentication.
196
+
197
+ Uses Django's session framework to authenticate users.
198
+ This requires Django to be configured and session middleware enabled.
199
+
200
+ Note: This has higher overhead than JWT/API key auth as it requires
201
+ Python execution for every request.
202
+ """
203
+
204
+ def __init__(self):
205
+ pass
206
+
207
+ @property
208
+ def scheme_name(self) -> str:
209
+ return "session"
210
+
211
+ def to_metadata(self) -> Dict[str, Any]:
212
+ return {
213
+ "type": "session",
214
+ }
215
+
216
+
217
+ def get_default_authentication_classes() -> List[BaseAuthentication]:
218
+ """
219
+ Get default authentication classes from Django settings.
220
+
221
+ Looks for BOLT_AUTHENTICATION_CLASSES in settings. If not found,
222
+ returns an empty list (no authentication by default).
223
+ """
224
+ try:
225
+ from django.conf import settings
226
+ from django.core.exceptions import ImproperlyConfigured
227
+ try:
228
+ if hasattr(settings, 'BOLT_AUTHENTICATION_CLASSES'):
229
+ return settings.BOLT_AUTHENTICATION_CLASSES
230
+ except ImproperlyConfigured:
231
+ # Settings not configured, return empty list
232
+ pass
233
+ except (ImportError, AttributeError):
234
+ pass
235
+
236
+ return []