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("# Conditional permissions module - placeholder".to_string())
788 }
789
790 fn generate_audit_module(&self) -> Result<String, Box<dyn std::error::Error>> {
791 Ok("# Audit module - placeholder".to_string())
792 }
793
794 fn generate_utils(&self) -> Result<String, Box<dyn std::error::Error>> {
795 Ok("# Utils module - placeholder".to_string())
796 }
797
798 fn generate_init(&self) -> Result<String, Box<dyn std::error::Error>> {
799 Ok("# Package init - placeholder".to_string())
800 }
801
802 fn generate_setup(&self) -> Result<String, Box<dyn std::error::Error>> {
803 Ok("# Setup.py - placeholder".to_string())
804 }
805
806 fn generate_requirements(&self) -> Result<String, Box<dyn std::error::Error>> {
807 Ok("aiohttp>=3.8.0\nrequests>=2.28.0".to_string())
808 }
809
810 fn generate_readme(&self) -> Result<String, Box<dyn std::error::Error>> {
811 Ok("# AuthFramework Python SDK - placeholder".to_string())
812 }
813}
814
815