turboapi 0.4.12__cp314-cp314t-macosx_11_0_arm64.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.
turboapi/security.py ADDED
@@ -0,0 +1,542 @@
1
+ """
2
+ FastAPI-compatible Security and Authentication for TurboAPI.
3
+
4
+ Includes:
5
+ - OAuth2 (Password Bearer, Authorization Code)
6
+ - HTTP Basic Authentication
7
+ - HTTP Bearer Authentication
8
+ - API Key Authentication (Header, Query, Cookie)
9
+ - Security scopes and dependencies
10
+ """
11
+
12
+ from typing import Optional, List, Dict, Any, Callable
13
+ from dataclasses import dataclass
14
+ import secrets
15
+ import base64
16
+
17
+
18
+ # ============================================================================
19
+ # Base Security Classes
20
+ # ============================================================================
21
+
22
+ class SecurityBase:
23
+ """Base class for all security schemes."""
24
+
25
+ def __init__(self, *, scheme_name: Optional[str] = None, auto_error: bool = True):
26
+ self.scheme_name = scheme_name
27
+ self.auto_error = auto_error
28
+
29
+
30
+ # ============================================================================
31
+ # OAuth2 Authentication
32
+ # ============================================================================
33
+
34
+ class OAuth2PasswordBearer(SecurityBase):
35
+ """
36
+ OAuth2 password bearer token authentication.
37
+
38
+ Usage:
39
+ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
40
+
41
+ @app.get("/users/me")
42
+ async def get_user(token: str = Depends(oauth2_scheme)):
43
+ return {"token": token}
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ tokenUrl: str,
49
+ scheme_name: Optional[str] = None,
50
+ scopes: Optional[Dict[str, str]] = None,
51
+ description: Optional[str] = None,
52
+ auto_error: bool = True,
53
+ ):
54
+ super().__init__(scheme_name=scheme_name, auto_error=auto_error)
55
+ self.tokenUrl = tokenUrl
56
+ self.scopes = scopes or {}
57
+ self.description = description
58
+ self.model = {
59
+ "type": "oauth2",
60
+ "flows": {
61
+ "password": {
62
+ "tokenUrl": tokenUrl,
63
+ "scopes": self.scopes,
64
+ }
65
+ },
66
+ }
67
+
68
+ def __call__(self, authorization: Optional[str] = None) -> Optional[str]:
69
+ """Extract token from Authorization header."""
70
+ if not authorization:
71
+ if self.auto_error:
72
+ raise HTTPException(
73
+ status_code=401,
74
+ detail="Not authenticated",
75
+ headers={"WWW-Authenticate": "Bearer"},
76
+ )
77
+ return None
78
+
79
+ scheme, _, token = authorization.partition(" ")
80
+ if scheme.lower() != "bearer":
81
+ if self.auto_error:
82
+ raise HTTPException(
83
+ status_code=401,
84
+ detail="Invalid authentication credentials",
85
+ headers={"WWW-Authenticate": "Bearer"},
86
+ )
87
+ return None
88
+
89
+ return token
90
+
91
+
92
+ @dataclass
93
+ class OAuth2PasswordRequestForm:
94
+ """
95
+ OAuth2 password request form data.
96
+
97
+ Automatically parses form data for OAuth2 password flow.
98
+ """
99
+ username: str
100
+ password: str
101
+ scope: str = ""
102
+ grant_type: Optional[str] = "password"
103
+ client_id: Optional[str] = None
104
+ client_secret: Optional[str] = None
105
+
106
+
107
+ class OAuth2AuthorizationCodeBearer(SecurityBase):
108
+ """
109
+ OAuth2 authorization code flow with bearer token.
110
+
111
+ Usage:
112
+ oauth2_scheme = OAuth2AuthorizationCodeBearer(
113
+ authorizationUrl="https://example.com/oauth/authorize",
114
+ tokenUrl="https://example.com/oauth/token"
115
+ )
116
+ """
117
+
118
+ def __init__(
119
+ self,
120
+ authorizationUrl: str,
121
+ tokenUrl: str,
122
+ refreshUrl: Optional[str] = None,
123
+ scheme_name: Optional[str] = None,
124
+ scopes: Optional[Dict[str, str]] = None,
125
+ description: Optional[str] = None,
126
+ auto_error: bool = True,
127
+ ):
128
+ super().__init__(scheme_name=scheme_name, auto_error=auto_error)
129
+ self.authorizationUrl = authorizationUrl
130
+ self.tokenUrl = tokenUrl
131
+ self.refreshUrl = refreshUrl
132
+ self.scopes = scopes or {}
133
+ self.description = description
134
+ self.model = {
135
+ "type": "oauth2",
136
+ "flows": {
137
+ "authorizationCode": {
138
+ "authorizationUrl": authorizationUrl,
139
+ "tokenUrl": tokenUrl,
140
+ "refreshUrl": refreshUrl,
141
+ "scopes": self.scopes,
142
+ }
143
+ },
144
+ }
145
+
146
+ def __call__(self, authorization: Optional[str] = None) -> Optional[str]:
147
+ """Extract token from Authorization header."""
148
+ if not authorization:
149
+ if self.auto_error:
150
+ raise HTTPException(
151
+ status_code=401,
152
+ detail="Not authenticated",
153
+ headers={"WWW-Authenticate": "Bearer"},
154
+ )
155
+ return None
156
+
157
+ scheme, _, token = authorization.partition(" ")
158
+ if scheme.lower() != "bearer":
159
+ if self.auto_error:
160
+ raise HTTPException(
161
+ status_code=401,
162
+ detail="Invalid authentication credentials",
163
+ headers={"WWW-Authenticate": "Bearer"},
164
+ )
165
+ return None
166
+
167
+ return token
168
+
169
+
170
+ # ============================================================================
171
+ # HTTP Basic Authentication
172
+ # ============================================================================
173
+
174
+ @dataclass
175
+ class HTTPBasicCredentials:
176
+ """HTTP Basic authentication credentials."""
177
+ username: str
178
+ password: str
179
+
180
+
181
+ class HTTPBasic(SecurityBase):
182
+ """
183
+ HTTP Basic authentication.
184
+
185
+ Usage:
186
+ security = HTTPBasic()
187
+
188
+ @app.get("/users/me")
189
+ def get_user(credentials: HTTPBasicCredentials = Depends(security)):
190
+ return {"username": credentials.username}
191
+ """
192
+
193
+ def __init__(
194
+ self,
195
+ *,
196
+ scheme_name: Optional[str] = None,
197
+ realm: Optional[str] = None,
198
+ auto_error: bool = True,
199
+ ):
200
+ super().__init__(scheme_name=scheme_name, auto_error=auto_error)
201
+ self.realm = realm
202
+ self.model = {"type": "http", "scheme": "basic"}
203
+
204
+ def __call__(self, authorization: Optional[str] = None) -> Optional[HTTPBasicCredentials]:
205
+ """Extract and decode Basic auth credentials."""
206
+ if not authorization:
207
+ if self.auto_error:
208
+ raise HTTPException(
209
+ status_code=401,
210
+ detail="Not authenticated",
211
+ headers={"WWW-Authenticate": f'Basic realm="{self.realm}"' if self.realm else "Basic"},
212
+ )
213
+ return None
214
+
215
+ scheme, _, credentials = authorization.partition(" ")
216
+ if scheme.lower() != "basic":
217
+ if self.auto_error:
218
+ raise HTTPException(
219
+ status_code=401,
220
+ detail="Invalid authentication credentials",
221
+ headers={"WWW-Authenticate": f'Basic realm="{self.realm}"' if self.realm else "Basic"},
222
+ )
223
+ return None
224
+
225
+ try:
226
+ decoded = base64.b64decode(credentials).decode("utf-8")
227
+ username, _, password = decoded.partition(":")
228
+ return HTTPBasicCredentials(username=username, password=password)
229
+ except Exception:
230
+ if self.auto_error:
231
+ raise HTTPException(
232
+ status_code=401,
233
+ detail="Invalid authentication credentials",
234
+ headers={"WWW-Authenticate": f'Basic realm="{self.realm}"' if self.realm else "Basic"},
235
+ )
236
+ return None
237
+
238
+
239
+ # ============================================================================
240
+ # HTTP Bearer Authentication
241
+ # ============================================================================
242
+
243
+ @dataclass
244
+ class HTTPAuthorizationCredentials:
245
+ """HTTP authorization credentials."""
246
+ scheme: str
247
+ credentials: str
248
+
249
+
250
+ class HTTPBearer(SecurityBase):
251
+ """
252
+ HTTP Bearer token authentication.
253
+
254
+ Usage:
255
+ security = HTTPBearer()
256
+
257
+ @app.get("/users/me")
258
+ def get_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
259
+ return {"token": credentials.credentials}
260
+ """
261
+
262
+ def __init__(
263
+ self,
264
+ *,
265
+ scheme_name: Optional[str] = None,
266
+ auto_error: bool = True,
267
+ ):
268
+ super().__init__(scheme_name=scheme_name, auto_error=auto_error)
269
+ self.model = {"type": "http", "scheme": "bearer"}
270
+
271
+ def __call__(self, authorization: Optional[str] = None) -> Optional[HTTPAuthorizationCredentials]:
272
+ """Extract Bearer token."""
273
+ if not authorization:
274
+ if self.auto_error:
275
+ raise HTTPException(
276
+ status_code=401,
277
+ detail="Not authenticated",
278
+ headers={"WWW-Authenticate": "Bearer"},
279
+ )
280
+ return None
281
+
282
+ scheme, _, credentials = authorization.partition(" ")
283
+ if scheme.lower() != "bearer":
284
+ if self.auto_error:
285
+ raise HTTPException(
286
+ status_code=401,
287
+ detail="Invalid authentication credentials",
288
+ headers={"WWW-Authenticate": "Bearer"},
289
+ )
290
+ return None
291
+
292
+ return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
293
+
294
+
295
+ class HTTPDigest(SecurityBase):
296
+ """
297
+ HTTP Digest authentication.
298
+
299
+ Usage:
300
+ security = HTTPDigest()
301
+ """
302
+
303
+ def __init__(
304
+ self,
305
+ *,
306
+ scheme_name: Optional[str] = None,
307
+ auto_error: bool = True,
308
+ ):
309
+ super().__init__(scheme_name=scheme_name, auto_error=auto_error)
310
+ self.model = {"type": "http", "scheme": "digest"}
311
+
312
+
313
+ # ============================================================================
314
+ # API Key Authentication
315
+ # ============================================================================
316
+
317
+ class APIKeyBase(SecurityBase):
318
+ """Base class for API key authentication."""
319
+
320
+ def __init__(
321
+ self,
322
+ *,
323
+ name: str,
324
+ scheme_name: Optional[str] = None,
325
+ description: Optional[str] = None,
326
+ auto_error: bool = True,
327
+ ):
328
+ super().__init__(scheme_name=scheme_name, auto_error=auto_error)
329
+ self.name = name
330
+ self.description = description
331
+
332
+
333
+ class APIKeyQuery(APIKeyBase):
334
+ """
335
+ API Key authentication via query parameter.
336
+
337
+ Usage:
338
+ api_key = APIKeyQuery(name="api_key")
339
+
340
+ @app.get("/items")
341
+ def get_items(key: str = Depends(api_key)):
342
+ return {"api_key": key}
343
+ """
344
+
345
+ def __init__(
346
+ self,
347
+ *,
348
+ name: str,
349
+ scheme_name: Optional[str] = None,
350
+ description: Optional[str] = None,
351
+ auto_error: bool = True,
352
+ ):
353
+ super().__init__(
354
+ name=name,
355
+ scheme_name=scheme_name,
356
+ description=description,
357
+ auto_error=auto_error,
358
+ )
359
+ self.model = {"type": "apiKey", "in": "query", "name": name}
360
+
361
+ def __call__(self, query_params: Optional[Dict[str, str]] = None) -> Optional[str]:
362
+ """Extract API key from query parameters."""
363
+ if not query_params or self.name not in query_params:
364
+ if self.auto_error:
365
+ raise HTTPException(
366
+ status_code=403,
367
+ detail="Not authenticated",
368
+ )
369
+ return None
370
+ return query_params[self.name]
371
+
372
+
373
+ class APIKeyHeader(APIKeyBase):
374
+ """
375
+ API Key authentication via HTTP header.
376
+
377
+ Usage:
378
+ api_key = APIKeyHeader(name="X-API-Key")
379
+
380
+ @app.get("/items")
381
+ def get_items(key: str = Depends(api_key)):
382
+ return {"api_key": key}
383
+ """
384
+
385
+ def __init__(
386
+ self,
387
+ *,
388
+ name: str,
389
+ scheme_name: Optional[str] = None,
390
+ description: Optional[str] = None,
391
+ auto_error: bool = True,
392
+ ):
393
+ super().__init__(
394
+ name=name,
395
+ scheme_name=scheme_name,
396
+ description=description,
397
+ auto_error=auto_error,
398
+ )
399
+ self.model = {"type": "apiKey", "in": "header", "name": name}
400
+
401
+ def __call__(self, headers: Optional[Dict[str, str]] = None) -> Optional[str]:
402
+ """Extract API key from headers."""
403
+ if not headers or self.name.lower() not in {k.lower(): v for k, v in headers.items()}:
404
+ if self.auto_error:
405
+ raise HTTPException(
406
+ status_code=403,
407
+ detail="Not authenticated",
408
+ )
409
+ return None
410
+
411
+ # Case-insensitive header lookup
412
+ for key, value in headers.items():
413
+ if key.lower() == self.name.lower():
414
+ return value
415
+ return None
416
+
417
+
418
+ class APIKeyCookie(APIKeyBase):
419
+ """
420
+ API Key authentication via HTTP cookie.
421
+
422
+ Usage:
423
+ api_key = APIKeyCookie(name="session")
424
+
425
+ @app.get("/items")
426
+ def get_items(key: str = Depends(api_key)):
427
+ return {"session": key}
428
+ """
429
+
430
+ def __init__(
431
+ self,
432
+ *,
433
+ name: str,
434
+ scheme_name: Optional[str] = None,
435
+ description: Optional[str] = None,
436
+ auto_error: bool = True,
437
+ ):
438
+ super().__init__(
439
+ name=name,
440
+ scheme_name=scheme_name,
441
+ description=description,
442
+ auto_error=auto_error,
443
+ )
444
+ self.model = {"type": "apiKey", "in": "cookie", "name": name}
445
+
446
+ def __call__(self, cookies: Optional[Dict[str, str]] = None) -> Optional[str]:
447
+ """Extract API key from cookies."""
448
+ if not cookies or self.name not in cookies:
449
+ if self.auto_error:
450
+ raise HTTPException(
451
+ status_code=403,
452
+ detail="Not authenticated",
453
+ )
454
+ return None
455
+ return cookies[self.name]
456
+
457
+
458
+ # ============================================================================
459
+ # Security Scopes
460
+ # ============================================================================
461
+
462
+ class SecurityScopes:
463
+ """
464
+ Security scopes for OAuth2 and other scope-based auth.
465
+
466
+ Usage:
467
+ def get_current_user(
468
+ security_scopes: SecurityScopes,
469
+ token: str = Depends(oauth2_scheme)
470
+ ):
471
+ if security_scopes.scopes:
472
+ authenticate_value = f'Bearer scope="{security_scopes.scope_str}"'
473
+ else:
474
+ authenticate_value = "Bearer"
475
+ # Validate token and scopes...
476
+ """
477
+
478
+ def __init__(self, scopes: Optional[List[str]] = None):
479
+ self.scopes = scopes or []
480
+ self.scope_str = " ".join(self.scopes)
481
+
482
+
483
+ # ============================================================================
484
+ # Helper Functions
485
+ # ============================================================================
486
+
487
+ class HTTPException(Exception):
488
+ """HTTP exception for authentication errors."""
489
+
490
+ def __init__(
491
+ self,
492
+ status_code: int,
493
+ detail: Any = None,
494
+ headers: Optional[Dict[str, str]] = None,
495
+ ):
496
+ self.status_code = status_code
497
+ self.detail = detail
498
+ self.headers = headers
499
+
500
+
501
+ def verify_password(plain_password: str, hashed_password: str) -> bool:
502
+ """
503
+ Verify a password against a hash.
504
+
505
+ Note: This is a placeholder. Use a proper password hashing library like:
506
+ - passlib with bcrypt
507
+ - argon2-cffi
508
+ """
509
+ # TODO: Implement with proper password hashing
510
+ return secrets.compare_digest(plain_password, hashed_password)
511
+
512
+
513
+ def get_password_hash(password: str) -> str:
514
+ """
515
+ Hash a password.
516
+
517
+ Note: This is a placeholder. Use a proper password hashing library.
518
+ """
519
+ # TODO: Implement with proper password hashing
520
+ return password # INSECURE - just for demo!
521
+
522
+
523
+ # ============================================================================
524
+ # Dependency Injection Helper
525
+ # ============================================================================
526
+
527
+ class Depends:
528
+ """
529
+ Dependency injection marker (compatible with FastAPI).
530
+
531
+ Usage:
532
+ def get_current_user(token: str = Depends(oauth2_scheme)):
533
+ return decode_token(token)
534
+
535
+ @app.get("/users/me")
536
+ def read_users_me(user = Depends(get_current_user)):
537
+ return user
538
+ """
539
+
540
+ def __init__(self, dependency: Optional[Callable] = None, *, use_cache: bool = True):
541
+ self.dependency = dependency
542
+ self.use_cache = use_cache