1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct PythonSdkConfig {
12 pub base_url: String,
14 pub version: String,
16 pub include_rbac: bool,
18 pub include_conditional_permissions: bool,
20 pub include_audit: bool,
22 pub client_name: String,
24 pub async_support: bool,
26 pub type_hints: bool,
28}
29
30impl Default for PythonSdkConfig {
31 fn default() -> Self {
32 Self {
33 base_url: "https://api.example.com".to_string(),
34 version: "v1".to_string(),
35 include_rbac: true,
36 include_conditional_permissions: true,
37 include_audit: true,
38 client_name: "AuthFrameworkClient".to_string(),
39 async_support: true,
40 type_hints: true,
41 }
42 }
43}
44
45pub struct PythonSdkGenerator {
47 config: PythonSdkConfig,
48}
49
50impl PythonSdkGenerator {
51 pub fn new(config: PythonSdkConfig) -> Self {
53 Self { config }
54 }
55
56 pub fn generate_sdk(&self) -> Result<HashMap<String, String>, Box<dyn std::error::Error>> {
58 let mut files = HashMap::new();
59
60 files.insert("client.py".to_string(), self.generate_base_client()?);
62
63 if self.config.type_hints {
65 files.insert("types.py".to_string(), self.generate_types()?);
66 }
67
68 if self.config.include_rbac {
70 files.insert("rbac.py".to_string(), self.generate_rbac_module()?);
71 }
72
73 if self.config.include_conditional_permissions {
75 files.insert(
76 "conditional.py".to_string(),
77 self.generate_conditional_module()?,
78 );
79 }
80
81 if self.config.include_audit {
83 files.insert("audit.py".to_string(), self.generate_audit_module()?);
84 }
85
86 files.insert("utils.py".to_string(), self.generate_utils()?);
88
89 files.insert("__init__.py".to_string(), self.generate_init()?);
91
92 files.insert("setup.py".to_string(), self.generate_setup()?);
94
95 files.insert(
97 "requirements.txt".to_string(),
98 self.generate_requirements()?,
99 );
100
101 files.insert("README.md".to_string(), self.generate_readme()?);
103
104 Ok(files)
105 }
106
107 fn generate_base_client(&self) -> Result<String, Box<dyn std::error::Error>> {
109 let client_code = format!(
110 r#"""
111Enhanced {} with RBAC Support
112Generated by AuthFramework SDK Generator
113"""
114
115import asyncio
116import json
117import time
118from typing import Dict, Any, Optional, Union
119{}
120import aiohttp
121import requests
122from urllib.parse import urljoin, urlencode
123
124
125class HttpError(Exception):
126 """HTTP error with status code and response data"""
127
128 def __init__(self, status: int, message: str, data: Optional[Dict[str, Any]] = None):
129 self.status = status
130 self.message = message
131 self.data = data
132 super().__init__(f"HTTP {{status}}: {{message}}")
133
134
135class ApiResponse:
136 """Wrapper for API responses"""
137
138 def __init__(self, success: bool, data: Optional[Any] = None,
139 error: Optional[str] = None, message: Optional[str] = None):
140 self.success = success
141 self.data = data
142 self.error = error
143 self.message = message
144
145
146class {}:
147 """Enhanced AuthFramework client with comprehensive RBAC support"""
148
149 def __init__(self, base_url: str, access_token: Optional[str] = None,
150 api_key: Optional[str] = None, timeout: int = 30,
151 retry_attempts: int = 3):
152 self.base_url = base_url.rstrip('/')
153 self.timeout = timeout
154 self.retry_attempts = retry_attempts
155 self.headers = {{
156 'Content-Type': 'application/json',
157 'User-Agent': 'AuthFramework-Python-SDK/1.0.0'
158 }}
159
160 if access_token:
161 self.headers['Authorization'] = f'Bearer {{access_token}}'
162
163 if api_key:
164 self.headers['X-API-Key'] = api_key
165
166 def set_access_token(self, token: str) -> None:
167 """Set authentication token"""
168 self.headers['Authorization'] = f'Bearer {{token}}'
169
170 def clear_access_token(self) -> None:
171 """Clear authentication token"""
172 self.headers.pop('Authorization', None)
173
174 def set_api_key(self, api_key: str) -> None:
175 """Set API key"""
176 self.headers['X-API-Key'] = api_key
177
178 def _make_request_sync(self, method: str, path: str, data: Optional[Dict] = None,
179 headers: Optional[Dict[str, str]] = None) -> ApiResponse:
180 """Make synchronous HTTP request with retry logic"""
181 url = urljoin(self.base_url, path)
182 request_headers = {{**self.headers, **(headers or {{}})}}
183
184 last_error = None
185
186 for attempt in range(self.retry_attempts + 1):
187 try:
188 response = requests.request(
189 method=method,
190 url=url,
191 headers=request_headers,
192 json=data,
193 timeout=self.timeout
194 )
195
196 response_data = response.json() if response.content else {{}}
197
198 if not response.ok:
199 raise HttpError(
200 status=response.status_code,
201 message=response.reason or 'Request failed',
202 data=response_data
203 )
204
205 return ApiResponse(
206 success=response_data.get('success', True),
207 data=response_data.get('data'),
208 error=response_data.get('error'),
209 message=response_data.get('message')
210 )
211
212 except (requests.RequestException, HttpError) as e:
213 last_error = e
214
215 # Don't retry on client errors (4xx)
216 if isinstance(e, HttpError) and 400 <= e.status < 500:
217 raise e
218
219 # Wait before retry (exponential backoff)
220 if attempt < self.retry_attempts:
221 time.sleep(2 ** attempt)
222
223 raise last_error or Exception('Request failed after all retries')
224
225 async def _make_request_async(self, method: str, path: str, data: Optional[Dict] = None,
226 headers: Optional[Dict[str, str]] = None) -> ApiResponse:
227 """Make asynchronous HTTP request with retry logic"""
228 url = urljoin(self.base_url, path)
229 request_headers = {{**self.headers, **(headers or {{}})}}
230
231 last_error = None
232
233 for attempt in range(self.retry_attempts + 1):
234 try:
235 timeout = aiohttp.ClientTimeout(total=self.timeout)
236
237 async with aiohttp.ClientSession(timeout=timeout) as session:
238 async with session.request(
239 method=method,
240 url=url,
241 headers=request_headers,
242 json=data
243 ) as response:
244 response_data = await response.json() if response.content_length else {{}}
245
246 if not response.ok:
247 raise HttpError(
248 status=response.status,
249 message=response.reason or 'Request failed',
250 data=response_data
251 )
252
253 return ApiResponse(
254 success=response_data.get('success', True),
255 data=response_data.get('data'),
256 error=response_data.get('error'),
257 message=response_data.get('message')
258 )
259
260 except (aiohttp.ClientError, HttpError) as e:
261 last_error = e
262
263 # Don't retry on client errors (4xx)
264 if isinstance(e, HttpError) and 400 <= e.status < 500:
265 raise e
266
267 # Wait before retry (exponential backoff)
268 if attempt < self.retry_attempts:
269 await asyncio.sleep(2 ** attempt)
270
271 raise last_error or Exception('Request failed after all retries')
272
273 def get(self, path: str, headers: Optional[Dict[str, str]] = None) -> ApiResponse:
274 """Synchronous GET request"""
275 return self._make_request_sync('GET', path, headers=headers)
276
277 def post(self, path: str, data: Optional[Dict] = None, headers: Optional[Dict[str, str]] = None) -> ApiResponse:
278 """Synchronous POST request"""
279 return self._make_request_sync('POST', path, data=data, headers=headers)
280
281 def put(self, path: str, data: Optional[Dict] = None, headers: Optional[Dict[str, str]] = None) -> ApiResponse:
282 """Synchronous PUT request"""
283 return self._make_request_sync('PUT', path, data=data, headers=headers)
284
285 def delete(self, path: str, headers: Optional[Dict[str, str]] = None) -> ApiResponse:
286 """Synchronous DELETE request"""
287 return self._make_request_sync('DELETE', path, headers=headers)
288
289 async def get_async(self, path: str, headers: Optional[Dict[str, str]] = None) -> ApiResponse:
290 """Asynchronous GET request"""
291 return await self._make_request_async('GET', path, headers=headers)
292
293 async def post_async(self, path: str, data: Optional[Dict] = None, headers: Optional[Dict[str, str]] = None) -> ApiResponse:
294 """Asynchronous POST request"""
295 return await self._make_request_async('POST', path, data=data, headers=headers)
296
297 async def put_async(self, path: str, data: Optional[Dict] = None, headers: Optional[Dict[str, str]] = None) -> ApiResponse:
298 """Asynchronous PUT request"""
299 return await self._make_request_async('PUT', path, data=data, headers=headers)
300
301 async def delete_async(self, path: str, headers: Optional[Dict[str, str]] = None) -> ApiResponse:
302 """Asynchronous DELETE request"""
303 return await self._make_request_async('DELETE', path, headers=headers)
304"#,
305 self.config.client_name,
306 if self.config.type_hints { ", List" } else { "" },
307 self.config.client_name
308 );
309
310 Ok(client_code)
311 }
312
313 fn generate_types(&self) -> Result<String, Box<dyn std::error::Error>> {
315 let types_code = r#"""
316Type definitions for AuthFramework RBAC
317"""
318
319from typing import Dict, List, Optional, Union, Any
320from datetime import datetime
321from dataclasses import dataclass
322from enum import Enum
323
324
325class TimeOfDay(Enum):
326 """Time of day classification"""
327 BUSINESS_HOURS = "business_hours"
328 AFTER_HOURS = "after_hours"
329 WEEKEND = "weekend"
330 HOLIDAY = "holiday"
331
332
333class DayType(Enum):
334 """Day type classification"""
335 WEEKDAY = "weekday"
336 WEEKEND = "weekend"
337 HOLIDAY = "holiday"
338
339
340class DeviceType(Enum):
341 """Device type classification"""
342 DESKTOP = "desktop"
343 MOBILE = "mobile"
344 TABLET = "tablet"
345 UNKNOWN = "unknown"
346
347
348class ConnectionType(Enum):
349 """Connection type classification"""
350 DIRECT = "direct"
351 VPN = "vpn"
352 PROXY = "proxy"
353 TOR = "tor"
354 CORPORATE = "corporate"
355 UNKNOWN = "unknown"
356
357
358class SecurityLevel(Enum):
359 """Security level assessment"""
360 LOW = "low"
361 MEDIUM = "medium"
362 HIGH = "high"
363 CRITICAL = "critical"
364
365
366@dataclass
367class Role:
368 """Role definition"""
369 id: str
370 name: str
371 description: Optional[str] = None
372 parent_id: Optional[str] = None
373 permissions: List[str] = None
374 created_at: Optional[datetime] = None
375 updated_at: Optional[datetime] = None
376
377 def __post_init__(self):
378 if self.permissions is None:
379 self.permissions = []
380
381
382@dataclass
383class Permission:
384 """Permission definition"""
385 id: str
386 action: str
387 resource: str
388 conditions: Optional[Dict[str, str]] = None
389 created_at: Optional[datetime] = None
390
391
392@dataclass
393class UserRole:
394 """User role assignment"""
395 role_id: str
396 role_name: str
397 assigned_at: datetime
398 assigned_by: Optional[str] = None
399 expires_at: Optional[datetime] = None
400
401
402@dataclass
403class UserRolesResponse:
404 """User roles and effective permissions"""
405 user_id: str
406 roles: List[UserRole]
407 effective_permissions: List[str]
408
409
410@dataclass
411class CreateRoleRequest:
412 """Request to create a new role"""
413 name: str
414 description: Optional[str] = None
415 parent_id: Optional[str] = None
416 permissions: Optional[List[str]] = None
417
418
419@dataclass
420class UpdateRoleRequest:
421 """Request to update an existing role"""
422 name: Optional[str] = None
423 description: Optional[str] = None
424 parent_id: Optional[str] = None
425
426
427@dataclass
428class AssignRoleRequest:
429 """Request to assign a role to a user"""
430 role_id: str
431 expires_at: Optional[datetime] = None
432 reason: Optional[str] = None
433
434
435@dataclass
436class BulkAssignment:
437 """Single assignment in bulk operation"""
438 user_id: str
439 role_id: str
440 expires_at: Optional[datetime] = None
441
442
443@dataclass
444class BulkAssignRequest:
445 """Request for bulk role assignment"""
446 assignments: List[BulkAssignment]
447
448
449@dataclass
450class ElevateRoleRequest:
451 """Request for role elevation"""
452 target_role: str
453 duration_minutes: Optional[int] = None
454 justification: str = ""
455
456
457@dataclass
458class PermissionCheckRequest:
459 """Request to check permissions"""
460 action: str
461 resource: str
462 context: Optional[Dict[str, str]] = None
463
464
465@dataclass
466class PermissionCheckResponse:
467 """Response from permission check"""
468 granted: bool
469 reason: str
470 required_roles: List[str]
471 missing_permissions: List[str]
472
473
474@dataclass
475class AuditEntry:
476 """Audit log entry"""
477 id: str
478 user_id: Optional[str]
479 action: str
480 resource: Optional[str]
481 result: str
482 context: Dict[str, str]
483 timestamp: datetime
484
485
486@dataclass
487class AuditLogResponse:
488 """Response containing audit logs"""
489 entries: List[AuditEntry]
490 total_count: int
491 page: int
492 per_page: int
493
494
495@dataclass
496class AuditQuery:
497 """Query parameters for audit logs"""
498 user_id: Optional[str] = None
499 action: Optional[str] = None
500 resource: Optional[str] = None
501 start_time: Optional[datetime] = None
502 end_time: Optional[datetime] = None
503 page: Optional[int] = None
504 per_page: Optional[int] = None
505
506
507@dataclass
508class ConditionalContext:
509 """Context for conditional permissions"""
510 time_of_day: Optional[TimeOfDay] = None
511 day_type: Optional[DayType] = None
512 device_type: Optional[DeviceType] = None
513 connection_type: Optional[ConnectionType] = None
514 security_level: Optional[SecurityLevel] = None
515 risk_score: Optional[int] = None
516 ip_address: Optional[str] = None
517 user_agent: Optional[str] = None
518 custom_attributes: Optional[Dict[str, str]] = None
519
520
521@dataclass
522class RoleListQuery:
523 """Query parameters for listing roles"""
524 page: Optional[int] = None
525 per_page: Optional[int] = None
526 parent_id: Optional[str] = None
527 include_permissions: Optional[bool] = None
528"#;
529
530 Ok(types_code.to_string())
531 }
532
533 fn generate_rbac_module(&self) -> Result<String, Box<dyn std::error::Error>> {
535 let rbac_code = format!(
536 r#"""
537RBAC (Role-Based Access Control) Module
538Provides comprehensive role and permission management
539"""
540
541from typing import Dict, List, Optional, Union
542{}
543from .types import (
544 Role, CreateRoleRequest, UpdateRoleRequest, UserRolesResponse,
545 AssignRoleRequest, BulkAssignRequest, ElevateRoleRequest,
546 PermissionCheckRequest, PermissionCheckResponse, RoleListQuery
547)
548from .client import ApiResponse
549
550
551class RbacManager:
552 """RBAC management client"""
553
554 def __init__(self, client):
555 self.client = client
556
557 # ============================================================================
558 # ROLE MANAGEMENT
559 # ============================================================================
560
561 def create_role(self, request: CreateRoleRequest) -> ApiResponse:
562 """Create a new role"""
563 data = {{
564 'name': request.name,
565 'description': request.description,
566 'parent_id': request.parent_id,
567 'permissions': request.permissions
568 }}
569 return self.client.post('/{}/rbac/roles', data)
570
571 def get_role(self, role_id: str) -> ApiResponse:
572 """Get role by ID"""
573 return self.client.get(f'/{}/rbac/roles/{{role_id}}')
574
575 def list_roles(self, query: Optional[RoleListQuery] = None) -> ApiResponse:
576 """List all roles with pagination"""
577 path = '/{}/rbac/roles'
578
579 if query:
580 params = []
581 if query.page is not None:
582 params.append(f'page={{query.page}}')
583 if query.per_page is not None:
584 params.append(f'per_page={{query.per_page}}')
585 if query.parent_id is not None:
586 params.append(f'parent_id={{query.parent_id}}')
587 if query.include_permissions is not None:
588 params.append(f'include_permissions={{str(query.include_permissions).lower()}}')
589
590 if params:
591 path += '?' + '&'.join(params)
592
593 return self.client.get(path)
594
595 def update_role(self, role_id: str, request: UpdateRoleRequest) -> ApiResponse:
596 """Update an existing role"""
597 data = {{}}
598 if request.name is not None:
599 data['name'] = request.name
600 if request.description is not None:
601 data['description'] = request.description
602 if request.parent_id is not None:
603 data['parent_id'] = request.parent_id
604
605 return self.client.put(f'/{}/rbac/roles/{{role_id}}', data)
606
607 def delete_role(self, role_id: str) -> ApiResponse:
608 """Delete a role"""
609 return self.client.delete(f'/{}/rbac/roles/{{role_id}}')
610
611 # ============================================================================
612 # USER ROLE ASSIGNMENTS
613 # ============================================================================
614
615 def assign_user_role(self, user_id: str, request: AssignRoleRequest) -> ApiResponse:
616 """Assign role to user"""
617 data = {{
618 'role_id': request.role_id,
619 'expires_at': request.expires_at.isoformat() if request.expires_at else None,
620 'reason': request.reason
621 }}
622 return self.client.post(f'/{}/rbac/users/{{user_id}}/roles', data)
623
624 def revoke_user_role(self, user_id: str, role_id: str) -> ApiResponse:
625 """Revoke role from user"""
626 return self.client.delete(f'/{}/rbac/users/{{user_id}}/roles/{{role_id}}')
627
628 def get_user_roles(self, user_id: str) -> ApiResponse:
629 """Get user's roles and effective permissions"""
630 return self.client.get(f'/{}/rbac/users/{{user_id}}/roles')
631
632 def bulk_assign_roles(self, request: BulkAssignRequest) -> ApiResponse:
633 """Bulk assign roles to multiple users"""
634 data = {{
635 'assignments': [
636 {{
637 'user_id': assignment.user_id,
638 'role_id': assignment.role_id,
639 'expires_at': assignment.expires_at.isoformat() if assignment.expires_at else None
640 }}
641 for assignment in request.assignments
642 ]
643 }}
644 return self.client.post('/{}/rbac/bulk/assign', data)
645
646 # ============================================================================
647 # PERMISSION CHECKING
648 # ============================================================================
649
650 def check_permission(self, request: PermissionCheckRequest) -> ApiResponse:
651 """Check if current user has permission"""
652 data = {{
653 'action': request.action,
654 'resource': request.resource,
655 'context': request.context
656 }}
657 return self.client.post('/{}/rbac/check-permission', data)
658
659 def has_permission(self, action: str, resource: str, context: Optional[Dict[str, str]] = None) -> bool:
660 """Quick permission check (returns boolean)"""
661 try:
662 request = PermissionCheckRequest(action=action, resource=resource, context=context)
663 response = self.check_permission(request)
664 return response.data and response.data.get('granted', False)
665 except Exception:
666 return False
667
668 def elevate_role(self, request: ElevateRoleRequest) -> ApiResponse:
669 """Request role elevation"""
670 data = {{
671 'target_role': request.target_role,
672 'duration_minutes': request.duration_minutes,
673 'justification': request.justification
674 }}
675 return self.client.post('/{}/rbac/elevate', data)
676
677 # ============================================================================
678 # ASYNC METHODS
679 # ============================================================================
680
681 async def create_role_async(self, request: CreateRoleRequest) -> ApiResponse:
682 """Create a new role (async)"""
683 data = {{
684 'name': request.name,
685 'description': request.description,
686 'parent_id': request.parent_id,
687 'permissions': request.permissions
688 }}
689 return await self.client.post_async('/{}/rbac/roles', data)
690
691 async def get_role_async(self, role_id: str) -> ApiResponse:
692 """Get role by ID (async)"""
693 return await self.client.get_async(f'/{}/rbac/roles/{{role_id}}')
694
695 async def has_permission_async(self, action: str, resource: str, context: Optional[Dict[str, str]] = None) -> bool:
696 """Quick permission check (async, returns boolean)"""
697 try:
698 request = PermissionCheckRequest(action=action, resource=resource, context=context)
699 data = {{
700 'action': request.action,
701 'resource': request.resource,
702 'context': request.context
703 }}
704 response = await self.client.post_async('/{}/rbac/check-permission', data)
705 return response.data and response.data.get('granted', False)
706 except Exception:
707 return False
708
709 # ============================================================================
710 # CONVENIENCE METHODS
711 # ============================================================================
712
713 def user_has_any_role(self, user_id: str, role_names: List[str]) -> bool:
714 """Check if user has any of the specified roles"""
715 try:
716 response = self.get_user_roles(user_id)
717 if not response.data:
718 return False
719
720 user_role_names = [role['role_name'] for role in response.data.get('roles', [])]
721 return any(role in user_role_names for role in role_names)
722 except Exception:
723 return False
724
725 def user_has_all_roles(self, user_id: str, role_names: List[str]) -> bool:
726 """Check if user has all of the specified roles"""
727 try:
728 response = self.get_user_roles(user_id)
729 if not response.data:
730 return False
731
732 user_role_names = [role['role_name'] for role in response.data.get('roles', [])]
733 return all(role in user_role_names for role in role_names)
734 except Exception:
735 return False
736
737 def get_role_hierarchy(self) -> Dict[str, List[str]]:
738 """Get role hierarchy (parent-child relationships)"""
739 try:
740 response = self.list_roles(RoleListQuery(include_permissions=False))
741 if not response.data:
742 return {{}}
743
744 hierarchy = {{}}
745
746 for role in response.data:
747 parent_id = role.get('parent_id')
748 if parent_id:
749 if parent_id not in hierarchy:
750 hierarchy[parent_id] = []
751 hierarchy[parent_id].append(role['id'])
752
753 return hierarchy
754 except Exception:
755 return {{}}
756
757 def get_child_roles(self, parent_role_id: str) -> List[Role]:
758 """Get all child roles for a given parent role"""
759 try:
760 response = self.list_roles(RoleListQuery(parent_id=parent_role_id))
761 return response.data or []
762 except Exception:
763 return []
764"#,
765 "", self.config.version,
767 self.config.version,
768 self.config.version,
769 self.config.version,
770 self.config.version,
771 self.config.version,
772 self.config.version,
773 self.config.version,
774 self.config.version,
775 self.config.version,
776 self.config.version,
777 self.config.version,
778 self.config.version,
779 self.config.version
780 );
781
782 Ok(rbac_code)
783 }
784
785 fn generate_conditional_module(&self) -> Result<String, Box<dyn std::error::Error>> {
787 Ok(format!(
788 r#""""Conditional permission helpers for auth-framework Python SDK v{version}."""
789
790from __future__ import annotations
791from typing import Any, Callable, List, Optional
792
793
794class ConditionalPermission:
795 """A permission that is only granted when a runtime condition is satisfied."""
796
797 def __init__(self, permission: str, condition: Callable[..., bool]) -> None:
798 self.permission = permission
799 self._condition = condition
800
801 def is_granted(self, **context: Any) -> bool:
802 """Return True only when the attached condition evaluates to True."""
803 return self._condition(**context)
804
805
806class PermissionGate:
807 """Evaluates a list of :class:`ConditionalPermission` items against a context."""
808
809 def __init__(self, permissions: Optional[List[ConditionalPermission]] = None) -> None:
810 self._permissions: List[ConditionalPermission] = permissions or []
811
812 def add(self, cp: ConditionalPermission) -> None:
813 self._permissions.append(cp)
814
815 def evaluate(self, **context: Any) -> List[str]:
816 """Return the names of all permissions granted in the given context."""
817 return [
818 cp.permission for cp in self._permissions if cp.is_granted(**context)
819 ]
820
821 def has(self, permission: str, **context: Any) -> bool:
822 """Return True if *permission* is granted in *context*."""
823 return permission in self.evaluate(**context)
824"#,
825 version = self.config.version
826 ))
827 }
828
829 fn generate_audit_module(&self) -> Result<String, Box<dyn std::error::Error>> {
830 Ok(format!(
831 r#"""Audit log client for auth-framework Python SDK v{version}."""
832
833from __future__ import annotations
834import time
835from dataclasses import dataclass, field
836from typing import Any, Dict, List, Optional
837
838
839@dataclass
840class AuditEvent:
841 """Represents a single audit log entry."""
842
843 event_type: str
844 user_id: str
845 resource: str
846 outcome: str
847 timestamp: float = field(default_factory=time.time)
848 metadata: Dict[str, Any] = field(default_factory=dict)
849
850
851class AuditLog:
852 """Lightweight in-process audit log buffer."""
853
854 def __init__(self, max_size: int = 10_000) -> None:
855 self._events: List[AuditEvent] = []
856 self._max_size = max_size
857
858 def record(self, event: AuditEvent) -> None:
859 if len(self._events) >= self._max_size:
860 self._events.pop(0)
861 self._events.append(event)
862
863 def query(
864 self,
865 *,
866 user_id: Optional[str] = None,
867 event_type: Optional[str] = None,
868 outcome: Optional[str] = None,
869 ) -> List[AuditEvent]:
870 results = self._events
871 if user_id is not None:
872 results = [e for e in results if e.user_id == user_id]
873 if event_type is not None:
874 results = [e for e in results if e.event_type == event_type]
875 if outcome is not None:
876 results = [e for e in results if e.outcome == outcome]
877 return results
878
879 def clear(self) -> None:
880 self._events.clear()
881"#,
882 version = self.config.version
883 ))
884 }
885
886 fn generate_utils(&self) -> Result<String, Box<dyn std::error::Error>> {
887 Ok(format!(
888 r#"""Utility helpers for auth-framework Python SDK v{version}."""
889
890from __future__ import annotations
891import base64
892import time
893from typing import Any, Dict, Optional
894
895
896def decode_jwt_payload(token: str) -> Dict[str, Any]:
897 """Decode JWT payload without verifying the signature.
898
899 .. warning::
900 This does **not** verify the token. Use the server-side validation
901 endpoints for security-sensitive operations.
902 """
903 try:
904 parts = token.split(".")
905 if len(parts) < 2:
906 raise ValueError("Not a valid JWT")
907 padding = "=" * (4 - len(parts[1]) % 4)
908 import json
909 return json.loads(base64.urlsafe_b64decode(parts[1] + padding))
910 except Exception as exc:
911 raise ValueError(f"Failed to decode JWT payload: {{exc}}") from exc
912
913
914def is_token_expired(token: str, leeway: int = 0) -> bool:
915 """Return True if the JWT *exp* claim is in the past."""
916 try:
917 payload = decode_jwt_payload(token)
918 exp: Optional[int] = payload.get("exp")
919 if exp is None:
920 return False
921 return time.time() > (exp + leeway)
922 except ValueError:
923 return True
924
925
926def constant_time_compare(a: str, b: str) -> bool:
927 """Compare two strings in constant time to prevent timing attacks."""
928 if len(a) != len(b):
929 return False
930 result = 0
931 for x, y in zip(a, b):
932 result |= ord(x) ^ ord(y)
933 return result == 0
934"#,
935 version = self.config.version
936 ))
937 }
938
939 fn generate_init(&self) -> Result<String, Box<dyn std::error::Error>> {
940 Ok(format!(
941 r#"""auth-framework Python SDK v{version}.
942
943Usage::
944
945 from auth_framework import AuthClient
946
947 client = AuthClient(base_url="https://auth.example.com", api_key="...")
948 token = client.authenticate(username="alice", password="secret")
949"""
950
951from .client import AuthClient
952from .models import AuthToken, UserInfo
953from .exceptions import AuthError, TokenExpiredError, PermissionDeniedError
954
955__all__ = [
956 "AuthClient",
957 "AuthToken",
958 "UserInfo",
959 "AuthError",
960 "TokenExpiredError",
961 "PermissionDeniedError",
962]
963
964__version__ = "{version}"
965"#,
966 version = self.config.version
967 ))
968 }
969
970 fn generate_setup(&self) -> Result<String, Box<dyn std::error::Error>> {
971 Ok(format!(
972 r#""""Setup configuration for auth-framework Python SDK."""
973from setuptools import setup, find_packages
974
975setup(
976 name="auth-framework-sdk",
977 version="{version}",
978 description="Python SDK for auth-framework authentication and authorization",
979 long_description=open("README.md").read(),
980 long_description_content_type="text/markdown",
981 python_requires=">=3.9",
982 packages=find_packages(),
983 install_requires=[
984 "aiohttp>=3.8.0",
985 "requests>=2.28.0",
986 ],
987 extras_require={{
988 "async": ["aiohttp>=3.8.0"],
989 }},
990 classifiers=[
991 "Programming Language :: Python :: 3",
992 "License :: OSI Approved :: MIT License",
993 "Topic :: Security",
994 ],
995)
996"#,
997 version = self.config.version,
998 ))
999 }
1000
1001 fn generate_requirements(&self) -> Result<String, Box<dyn std::error::Error>> {
1002 Ok("aiohttp>=3.8.0\nrequests>=2.28.0".to_string())
1003 }
1004
1005 fn generate_readme(&self) -> Result<String, Box<dyn std::error::Error>> {
1006 Ok(format!(
1007 r#"# auth-framework-sdk — AuthFramework Python SDK v{version}
1008
1009A Python SDK for integrating with the AuthFramework authentication and authorization server.
1010
1011## Installation
1012
1013```bash
1014pip install auth-framework-sdk
1015```
1016
1017## Quick Start
1018
1019```python
1020from auth_framework import {client_name}
1021
1022client = {client_name}(
1023 base_url="{base_url}",
1024 api_key="your-api-key",
1025)
1026
1027# Authenticate a user
1028token = client.authenticate(username="alice", password="secret")
1029print(token.access_token)
1030
1031# Validate a token
1032user = client.validate_token(token.access_token)
1033print(user.user_id)
1034```
1035
1036## Features
1037
1038- **Token Authentication**: Username/password, API keys, OAuth 2.0
1039- **RBAC**: Role-based access control with permission checks
1040- **MFA**: Multi-factor authentication (TOTP, Email, WebAuthn)
1041- **Audit Logging**: Built-in audit log client
1042- **Async Support**: Full async/await support via `aiohttp`
1043
1044## License
1045
1046MIT
1047"#,
1048 version = self.config.version,
1049 client_name = self.config.client_name,
1050 base_url = self.config.base_url,
1051 ))
1052 }
1053}
1054
1055#[cfg(test)]
1056mod tests {
1057 use super::*;
1058
1059 #[test]
1060 fn test_python_sdk_config_default() {
1061 let config = PythonSdkConfig::default();
1062 assert_eq!(config.base_url, "https://api.example.com");
1063 assert_eq!(config.version, "v1");
1064 assert!(config.include_rbac);
1065 assert!(config.include_conditional_permissions);
1066 assert!(config.include_audit);
1067 assert_eq!(config.client_name, "AuthFrameworkClient");
1068 assert!(config.async_support);
1069 assert!(config.type_hints);
1070 }
1071
1072 #[test]
1073 fn test_generate_sdk_default_config() {
1074 let generator = PythonSdkGenerator::new(PythonSdkConfig::default());
1075 let files = generator.generate_sdk().unwrap();
1076 assert!(files.contains_key("client.py"));
1078 assert!(files.contains_key("types.py"));
1079 assert!(files.contains_key("rbac.py"));
1080 assert!(files.contains_key("conditional.py"));
1081 assert!(files.contains_key("audit.py"));
1082 assert!(files.contains_key("utils.py"));
1083 assert!(files.contains_key("__init__.py"));
1084 assert!(files.contains_key("setup.py"));
1085 assert!(files.contains_key("requirements.txt"));
1086 assert!(files.contains_key("README.md"));
1087 }
1088
1089 #[test]
1090 fn test_generate_sdk_without_optional_modules() {
1091 let config = PythonSdkConfig {
1092 include_rbac: false,
1093 include_conditional_permissions: false,
1094 include_audit: false,
1095 type_hints: false,
1096 ..PythonSdkConfig::default()
1097 };
1098 let generator = PythonSdkGenerator::new(config);
1099 let files = generator.generate_sdk().unwrap();
1100 assert!(files.contains_key("client.py"));
1101 assert!(!files.contains_key("types.py"));
1102 assert!(!files.contains_key("rbac.py"));
1103 assert!(!files.contains_key("conditional.py"));
1104 assert!(!files.contains_key("audit.py"));
1105 }
1106
1107 #[test]
1108 fn test_generated_client_contains_class_name() {
1109 let config = PythonSdkConfig {
1110 client_name: "MyClient".to_string(),
1111 ..PythonSdkConfig::default()
1112 };
1113 let generator = PythonSdkGenerator::new(config);
1114 let files = generator.generate_sdk().unwrap();
1115 let client_code = &files["client.py"];
1116 assert!(client_code.contains("MyClient"));
1117 }
1118}