isa-model 0.3.91__py3-none-any.whl → 0.4.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. isa_model/client.py +1166 -584
  2. isa_model/core/cache/redis_cache.py +410 -0
  3. isa_model/core/config/config_manager.py +282 -12
  4. isa_model/core/config.py +91 -1
  5. isa_model/core/database/__init__.py +1 -0
  6. isa_model/core/database/direct_db_client.py +114 -0
  7. isa_model/core/database/migration_manager.py +563 -0
  8. isa_model/core/database/migrations.py +297 -0
  9. isa_model/core/database/supabase_client.py +258 -0
  10. isa_model/core/dependencies.py +316 -0
  11. isa_model/core/discovery/__init__.py +19 -0
  12. isa_model/core/discovery/consul_discovery.py +190 -0
  13. isa_model/core/logging/__init__.py +54 -0
  14. isa_model/core/logging/influx_logger.py +523 -0
  15. isa_model/core/logging/loki_logger.py +160 -0
  16. isa_model/core/models/__init__.py +46 -0
  17. isa_model/core/models/config_models.py +625 -0
  18. isa_model/core/models/deployment_billing_tracker.py +430 -0
  19. isa_model/core/models/model_billing_tracker.py +60 -88
  20. isa_model/core/models/model_manager.py +66 -25
  21. isa_model/core/models/model_metadata.py +690 -0
  22. isa_model/core/models/model_repo.py +217 -55
  23. isa_model/core/models/model_statistics_tracker.py +234 -0
  24. isa_model/core/models/model_storage.py +0 -1
  25. isa_model/core/models/model_version_manager.py +959 -0
  26. isa_model/core/models/system_models.py +857 -0
  27. isa_model/core/pricing_manager.py +2 -249
  28. isa_model/core/repositories/__init__.py +9 -0
  29. isa_model/core/repositories/config_repository.py +912 -0
  30. isa_model/core/resilience/circuit_breaker.py +366 -0
  31. isa_model/core/security/secrets.py +358 -0
  32. isa_model/core/services/__init__.py +2 -4
  33. isa_model/core/services/intelligent_model_selector.py +479 -370
  34. isa_model/core/storage/hf_storage.py +2 -2
  35. isa_model/core/types.py +8 -0
  36. isa_model/deployment/__init__.py +5 -48
  37. isa_model/deployment/core/__init__.py +2 -31
  38. isa_model/deployment/core/deployment_manager.py +1278 -368
  39. isa_model/deployment/local/__init__.py +31 -0
  40. isa_model/deployment/local/config.py +248 -0
  41. isa_model/deployment/local/gpu_gateway.py +607 -0
  42. isa_model/deployment/local/health_checker.py +428 -0
  43. isa_model/deployment/local/provider.py +586 -0
  44. isa_model/deployment/local/tensorrt_service.py +621 -0
  45. isa_model/deployment/local/transformers_service.py +644 -0
  46. isa_model/deployment/local/vllm_service.py +527 -0
  47. isa_model/deployment/modal/__init__.py +8 -0
  48. isa_model/deployment/modal/config.py +136 -0
  49. isa_model/deployment/modal/deployer.py +894 -0
  50. isa_model/deployment/modal/services/__init__.py +3 -0
  51. isa_model/deployment/modal/services/audio/__init__.py +1 -0
  52. isa_model/deployment/modal/services/audio/isa_audio_chatTTS_service.py +520 -0
  53. isa_model/deployment/modal/services/audio/isa_audio_openvoice_service.py +758 -0
  54. isa_model/deployment/modal/services/audio/isa_audio_service_v2.py +1044 -0
  55. isa_model/deployment/modal/services/embedding/__init__.py +1 -0
  56. isa_model/deployment/modal/services/embedding/isa_embed_rerank_service.py +296 -0
  57. isa_model/deployment/modal/services/llm/__init__.py +1 -0
  58. isa_model/deployment/modal/services/llm/isa_llm_service.py +424 -0
  59. isa_model/deployment/modal/services/video/__init__.py +1 -0
  60. isa_model/deployment/modal/services/video/isa_video_hunyuan_service.py +423 -0
  61. isa_model/deployment/modal/services/vision/__init__.py +1 -0
  62. isa_model/deployment/modal/services/vision/isa_vision_ocr_service.py +519 -0
  63. isa_model/deployment/modal/services/vision/isa_vision_qwen25_service.py +709 -0
  64. isa_model/deployment/modal/services/vision/isa_vision_table_service.py +676 -0
  65. isa_model/deployment/modal/services/vision/isa_vision_ui_service.py +833 -0
  66. isa_model/deployment/modal/services/vision/isa_vision_ui_service_optimized.py +660 -0
  67. isa_model/deployment/models/org-org-acme-corp-tenant-a-service-llm-20250825-225822/tenant-a-service_modal_service.py +48 -0
  68. isa_model/deployment/models/org-test-org-123-prefix-test-service-llm-20250825-225822/prefix-test-service_modal_service.py +48 -0
  69. isa_model/deployment/models/test-llm-service-llm-20250825-204442/test-llm-service_modal_service.py +48 -0
  70. isa_model/deployment/models/test-monitoring-gpt2-llm-20250825-212906/test-monitoring-gpt2_modal_service.py +48 -0
  71. isa_model/deployment/models/test-monitoring-gpt2-llm-20250825-213009/test-monitoring-gpt2_modal_service.py +48 -0
  72. isa_model/deployment/storage/__init__.py +5 -0
  73. isa_model/deployment/storage/deployment_repository.py +824 -0
  74. isa_model/deployment/triton/__init__.py +10 -0
  75. isa_model/deployment/triton/config.py +196 -0
  76. isa_model/deployment/triton/configs/__init__.py +1 -0
  77. isa_model/deployment/triton/provider.py +512 -0
  78. isa_model/deployment/triton/scripts/__init__.py +1 -0
  79. isa_model/deployment/triton/templates/__init__.py +1 -0
  80. isa_model/inference/__init__.py +47 -1
  81. isa_model/inference/ai_factory.py +179 -16
  82. isa_model/inference/legacy_services/__init__.py +21 -0
  83. isa_model/inference/legacy_services/model_evaluation.py +637 -0
  84. isa_model/inference/legacy_services/model_service.py +573 -0
  85. isa_model/inference/legacy_services/model_serving.py +717 -0
  86. isa_model/inference/legacy_services/model_training.py +561 -0
  87. isa_model/inference/models/__init__.py +21 -0
  88. isa_model/inference/models/inference_config.py +551 -0
  89. isa_model/inference/models/inference_record.py +675 -0
  90. isa_model/inference/models/performance_models.py +714 -0
  91. isa_model/inference/repositories/__init__.py +9 -0
  92. isa_model/inference/repositories/inference_repository.py +828 -0
  93. isa_model/inference/services/audio/__init__.py +21 -0
  94. isa_model/inference/services/audio/base_realtime_service.py +225 -0
  95. isa_model/inference/services/audio/base_stt_service.py +184 -11
  96. isa_model/inference/services/audio/isa_tts_service.py +0 -0
  97. isa_model/inference/services/audio/openai_realtime_service.py +320 -124
  98. isa_model/inference/services/audio/openai_stt_service.py +53 -11
  99. isa_model/inference/services/base_service.py +17 -1
  100. isa_model/inference/services/custom_model_manager.py +277 -0
  101. isa_model/inference/services/embedding/__init__.py +13 -0
  102. isa_model/inference/services/embedding/base_embed_service.py +111 -8
  103. isa_model/inference/services/embedding/isa_embed_service.py +305 -0
  104. isa_model/inference/services/embedding/ollama_embed_service.py +15 -3
  105. isa_model/inference/services/embedding/openai_embed_service.py +2 -4
  106. isa_model/inference/services/embedding/resilient_embed_service.py +285 -0
  107. isa_model/inference/services/embedding/tests/test_embedding.py +222 -0
  108. isa_model/inference/services/img/__init__.py +2 -2
  109. isa_model/inference/services/img/base_image_gen_service.py +24 -7
  110. isa_model/inference/services/img/replicate_image_gen_service.py +84 -422
  111. isa_model/inference/services/img/services/replicate_face_swap.py +193 -0
  112. isa_model/inference/services/img/services/replicate_flux.py +226 -0
  113. isa_model/inference/services/img/services/replicate_flux_kontext.py +219 -0
  114. isa_model/inference/services/img/services/replicate_sticker_maker.py +249 -0
  115. isa_model/inference/services/img/tests/test_img_client.py +297 -0
  116. isa_model/inference/services/llm/__init__.py +10 -2
  117. isa_model/inference/services/llm/base_llm_service.py +361 -26
  118. isa_model/inference/services/llm/cerebras_llm_service.py +628 -0
  119. isa_model/inference/services/llm/helpers/llm_adapter.py +71 -12
  120. isa_model/inference/services/llm/helpers/llm_prompts.py +342 -0
  121. isa_model/inference/services/llm/helpers/llm_utils.py +321 -23
  122. isa_model/inference/services/llm/huggingface_llm_service.py +581 -0
  123. isa_model/inference/services/llm/local_llm_service.py +747 -0
  124. isa_model/inference/services/llm/ollama_llm_service.py +11 -3
  125. isa_model/inference/services/llm/openai_llm_service.py +670 -56
  126. isa_model/inference/services/llm/yyds_llm_service.py +10 -3
  127. isa_model/inference/services/vision/__init__.py +27 -6
  128. isa_model/inference/services/vision/base_vision_service.py +118 -185
  129. isa_model/inference/services/vision/blip_vision_service.py +359 -0
  130. isa_model/inference/services/vision/helpers/image_utils.py +19 -10
  131. isa_model/inference/services/vision/isa_vision_service.py +634 -0
  132. isa_model/inference/services/vision/openai_vision_service.py +19 -10
  133. isa_model/inference/services/vision/tests/test_ocr_client.py +284 -0
  134. isa_model/inference/services/vision/vgg16_vision_service.py +257 -0
  135. isa_model/serving/api/cache_manager.py +245 -0
  136. isa_model/serving/api/dependencies/__init__.py +1 -0
  137. isa_model/serving/api/dependencies/auth.py +194 -0
  138. isa_model/serving/api/dependencies/database.py +139 -0
  139. isa_model/serving/api/error_handlers.py +284 -0
  140. isa_model/serving/api/fastapi_server.py +240 -18
  141. isa_model/serving/api/middleware/auth.py +317 -0
  142. isa_model/serving/api/middleware/security.py +268 -0
  143. isa_model/serving/api/middleware/tenant_context.py +414 -0
  144. isa_model/serving/api/routes/analytics.py +489 -0
  145. isa_model/serving/api/routes/config.py +645 -0
  146. isa_model/serving/api/routes/deployment_billing.py +315 -0
  147. isa_model/serving/api/routes/deployments.py +475 -0
  148. isa_model/serving/api/routes/gpu_gateway.py +440 -0
  149. isa_model/serving/api/routes/health.py +32 -12
  150. isa_model/serving/api/routes/inference_monitoring.py +486 -0
  151. isa_model/serving/api/routes/local_deployments.py +448 -0
  152. isa_model/serving/api/routes/logs.py +430 -0
  153. isa_model/serving/api/routes/settings.py +582 -0
  154. isa_model/serving/api/routes/tenants.py +575 -0
  155. isa_model/serving/api/routes/unified.py +992 -171
  156. isa_model/serving/api/routes/webhooks.py +479 -0
  157. isa_model/serving/api/startup.py +318 -0
  158. isa_model/serving/modal_proxy_server.py +249 -0
  159. isa_model/utils/gpu_utils.py +311 -0
  160. {isa_model-0.3.91.dist-info → isa_model-0.4.3.dist-info}/METADATA +76 -22
  161. isa_model-0.4.3.dist-info/RECORD +193 -0
  162. isa_model/deployment/cloud/__init__.py +0 -9
  163. isa_model/deployment/cloud/modal/__init__.py +0 -10
  164. isa_model/deployment/cloud/modal/isa_vision_doc_service.py +0 -766
  165. isa_model/deployment/cloud/modal/isa_vision_table_service.py +0 -532
  166. isa_model/deployment/cloud/modal/isa_vision_ui_service.py +0 -406
  167. isa_model/deployment/cloud/modal/register_models.py +0 -321
  168. isa_model/deployment/core/deployment_config.py +0 -356
  169. isa_model/deployment/core/isa_deployment_service.py +0 -401
  170. isa_model/deployment/gpu_int8_ds8/app/server.py +0 -66
  171. isa_model/deployment/gpu_int8_ds8/scripts/test_client.py +0 -43
  172. isa_model/deployment/gpu_int8_ds8/scripts/test_client_os.py +0 -35
  173. isa_model/deployment/runtime/deployed_service.py +0 -338
  174. isa_model/deployment/services/__init__.py +0 -9
  175. isa_model/deployment/services/auto_deploy_vision_service.py +0 -538
  176. isa_model/deployment/services/model_service.py +0 -332
  177. isa_model/deployment/services/service_monitor.py +0 -356
  178. isa_model/deployment/services/service_registry.py +0 -527
  179. isa_model/eval/__init__.py +0 -92
  180. isa_model/eval/benchmarks.py +0 -469
  181. isa_model/eval/config/__init__.py +0 -10
  182. isa_model/eval/config/evaluation_config.py +0 -108
  183. isa_model/eval/evaluators/__init__.py +0 -18
  184. isa_model/eval/evaluators/base_evaluator.py +0 -503
  185. isa_model/eval/evaluators/llm_evaluator.py +0 -472
  186. isa_model/eval/factory.py +0 -531
  187. isa_model/eval/infrastructure/__init__.py +0 -24
  188. isa_model/eval/infrastructure/experiment_tracker.py +0 -466
  189. isa_model/eval/metrics.py +0 -798
  190. isa_model/inference/adapter/unified_api.py +0 -248
  191. isa_model/inference/services/helpers/stacked_config.py +0 -148
  192. isa_model/inference/services/img/flux_professional_service.py +0 -603
  193. isa_model/inference/services/img/helpers/base_stacked_service.py +0 -274
  194. isa_model/inference/services/others/table_transformer_service.py +0 -61
  195. isa_model/inference/services/vision/doc_analysis_service.py +0 -640
  196. isa_model/inference/services/vision/helpers/base_stacked_service.py +0 -274
  197. isa_model/inference/services/vision/ui_analysis_service.py +0 -823
  198. isa_model/scripts/inference_tracker.py +0 -283
  199. isa_model/scripts/mlflow_manager.py +0 -379
  200. isa_model/scripts/model_registry.py +0 -465
  201. isa_model/scripts/register_models.py +0 -370
  202. isa_model/scripts/register_models_with_embeddings.py +0 -510
  203. isa_model/scripts/start_mlflow.py +0 -95
  204. isa_model/scripts/training_tracker.py +0 -257
  205. isa_model/training/__init__.py +0 -74
  206. isa_model/training/annotation/annotation_schema.py +0 -47
  207. isa_model/training/annotation/processors/annotation_processor.py +0 -126
  208. isa_model/training/annotation/storage/dataset_manager.py +0 -131
  209. isa_model/training/annotation/storage/dataset_schema.py +0 -44
  210. isa_model/training/annotation/tests/test_annotation_flow.py +0 -109
  211. isa_model/training/annotation/tests/test_minio copy.py +0 -113
  212. isa_model/training/annotation/tests/test_minio_upload.py +0 -43
  213. isa_model/training/annotation/views/annotation_controller.py +0 -158
  214. isa_model/training/cloud/__init__.py +0 -22
  215. isa_model/training/cloud/job_orchestrator.py +0 -402
  216. isa_model/training/cloud/runpod_trainer.py +0 -454
  217. isa_model/training/cloud/storage_manager.py +0 -482
  218. isa_model/training/core/__init__.py +0 -23
  219. isa_model/training/core/config.py +0 -181
  220. isa_model/training/core/dataset.py +0 -222
  221. isa_model/training/core/trainer.py +0 -720
  222. isa_model/training/core/utils.py +0 -213
  223. isa_model/training/factory.py +0 -424
  224. isa_model-0.3.91.dist-info/RECORD +0 -138
  225. /isa_model/{core/storage/minio_storage.py → deployment/modal/services/audio/isa_audio_fish_service.py} +0 -0
  226. /isa_model/deployment/{services → modal/services/vision}/simple_auto_deploy_vision_service.py +0 -0
  227. {isa_model-0.3.91.dist-info → isa_model-0.4.3.dist-info}/WHEEL +0 -0
  228. {isa_model-0.3.91.dist-info → isa_model-0.4.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,317 @@
1
+ """
2
+ Optional Authentication Middleware
3
+
4
+ Provides optional API key authentication for the ISA Model Platform.
5
+ When authentication is disabled (default), all endpoints remain open.
6
+ When enabled, requires API keys for access.
7
+ """
8
+
9
+ from fastapi import HTTPException, status, Depends, Request
10
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials, APIKeyHeader, APIKeyQuery
11
+ from typing import Optional, Dict, List, Union
12
+ import hashlib
13
+ import secrets
14
+ import time
15
+ import logging
16
+ import os
17
+ import json
18
+ from pathlib import Path
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ # Configuration
23
+ AUTH_ENABLED = os.getenv("REQUIRE_API_KEYS", "false").lower() == "true"
24
+ API_KEYS_FILE = Path(os.path.dirname(__file__)).parent.parent.parent / "deployment" / "dev" / ".api_keys.json"
25
+
26
+ # Security schemes (only used when auth is enabled)
27
+ bearer_scheme = HTTPBearer(auto_error=False)
28
+ api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
29
+ api_key_query = APIKeyQuery(name="api_key", auto_error=False)
30
+
31
+ class APIKeyManager:
32
+ def __init__(self):
33
+ self.api_keys: Dict[str, Dict] = {}
34
+
35
+ # Load API keys first to check if auth should be enabled
36
+ self.load_api_keys()
37
+
38
+ # Determine auth state: check explicit setting first, then auto-detect from keys
39
+ explicit_auth = AUTH_ENABLED
40
+ has_keys = len(self.api_keys) > 0
41
+
42
+ # If explicitly disabled (REQUIRE_API_KEYS=false), respect that setting
43
+ if os.getenv("REQUIRE_API_KEYS", "").lower() == "false":
44
+ self.auth_enabled = False
45
+ else:
46
+ # Otherwise, enable if explicitly set OR if API keys exist
47
+ self.auth_enabled = explicit_auth or has_keys
48
+
49
+ if self.auth_enabled:
50
+ logger.info(f"API Key authentication is ENABLED ({'explicit' if explicit_auth else 'auto-detected from keys'})")
51
+ else:
52
+ logger.info("API Key authentication is DISABLED - all endpoints are open")
53
+
54
+ def load_api_keys(self):
55
+ """Load API keys from file"""
56
+ try:
57
+ if API_KEYS_FILE.exists():
58
+ with open(API_KEYS_FILE, 'r') as f:
59
+ self.api_keys = json.load(f)
60
+ logger.info(f"Loaded {len(self.api_keys)} API keys")
61
+ else:
62
+ self.api_keys = {}
63
+ logger.info("No API keys file found - authentication will be disabled")
64
+ except Exception as e:
65
+ logger.error(f"Error loading API keys: {e}")
66
+ self.api_keys = {}
67
+
68
+ def save_api_keys(self):
69
+ """Save API keys to file"""
70
+ try:
71
+ API_KEYS_FILE.parent.mkdir(parents=True, exist_ok=True)
72
+ with open(API_KEYS_FILE, 'w') as f:
73
+ json.dump(self.api_keys, f, indent=2)
74
+ logger.info("API keys saved successfully")
75
+ except Exception as e:
76
+ logger.error(f"Error saving API keys: {e}")
77
+
78
+ def create_default_keys(self):
79
+ """Create default API keys for initial setup"""
80
+ admin_key = self.generate_api_key("admin", scopes=["read", "write", "admin"])
81
+ dev_key = self.generate_api_key("development", scopes=["read", "write"])
82
+
83
+ logger.warning("=== CREATED DEFAULT API KEYS ===")
84
+ logger.warning(f"Admin API Key: {admin_key}")
85
+ logger.warning(f"Development API Key: {dev_key}")
86
+ logger.warning("Please save these keys securely!")
87
+ logger.warning("=====================================")
88
+
89
+ return {"admin_key": admin_key, "dev_key": dev_key}
90
+
91
+ def generate_api_key(self, name: str, scopes: List[str] = None) -> str:
92
+ """Generate a new API key"""
93
+ if scopes is None:
94
+ scopes = ["read"]
95
+
96
+ # Generate secure random key
97
+ key = f"isa_{secrets.token_urlsafe(32)}"
98
+ key_hash = hashlib.sha256(key.encode()).hexdigest()
99
+
100
+ # Store key metadata
101
+ self.api_keys[key_hash] = {
102
+ "name": name,
103
+ "scopes": scopes,
104
+ "created_at": time.time(),
105
+ "last_used": None,
106
+ "usage_count": 0,
107
+ "active": True
108
+ }
109
+
110
+ self.save_api_keys()
111
+ return key
112
+
113
+ def validate_api_key(self, api_key: str) -> Optional[Dict]:
114
+ """Validate an API key and return its metadata"""
115
+ if not self.auth_enabled:
116
+ # When auth is disabled, return a default user context
117
+ return {
118
+ "name": "anonymous",
119
+ "scopes": ["read", "write", "admin"],
120
+ "auth_enabled": False
121
+ }
122
+
123
+ if not api_key:
124
+ return None
125
+
126
+ key_hash = hashlib.sha256(api_key.encode()).hexdigest()
127
+ key_data = self.api_keys.get(key_hash)
128
+
129
+ if not key_data or not key_data.get("active", True):
130
+ return None
131
+
132
+ # Update usage statistics
133
+ key_data["last_used"] = time.time()
134
+ key_data["usage_count"] = key_data.get("usage_count", 0) + 1
135
+ key_data["auth_enabled"] = True
136
+ self.save_api_keys()
137
+
138
+ return key_data
139
+
140
+ def revoke_api_key(self, api_key: str) -> bool:
141
+ """Revoke an API key"""
142
+ if not self.auth_enabled:
143
+ return False
144
+
145
+ key_hash = hashlib.sha256(api_key.encode()).hexdigest()
146
+ if key_hash in self.api_keys:
147
+ self.api_keys[key_hash]["active"] = False
148
+ self.save_api_keys()
149
+ return True
150
+ return False
151
+
152
+ def list_api_keys(self) -> List[Dict]:
153
+ """List all API keys (without revealing the actual keys)"""
154
+ if not self.auth_enabled:
155
+ return []
156
+
157
+ return [
158
+ {
159
+ "key_hash": key_hash[:16] + "...",
160
+ "name": data["name"],
161
+ "scopes": data["scopes"],
162
+ "created_at": data["created_at"],
163
+ "last_used": data.get("last_used"),
164
+ "usage_count": data.get("usage_count", 0),
165
+ "active": data.get("active", True)
166
+ }
167
+ for key_hash, data in self.api_keys.items()
168
+ ]
169
+
170
+ def enable_auth(self):
171
+ """Enable authentication"""
172
+ self.auth_enabled = True
173
+ if not self.api_keys:
174
+ return self.create_default_keys()
175
+ return None
176
+
177
+ def disable_auth(self):
178
+ """Disable authentication"""
179
+ self.auth_enabled = False
180
+
181
+ # Global API key manager instance
182
+ api_key_manager = APIKeyManager()
183
+
184
+ async def get_api_key_from_request(
185
+ request: Request,
186
+ bearer_token: Optional[HTTPAuthorizationCredentials] = Depends(bearer_scheme),
187
+ header_key: Optional[str] = Depends(api_key_header),
188
+ query_key: Optional[str] = Depends(api_key_query)
189
+ ) -> Optional[str]:
190
+ """Extract API key from various sources"""
191
+
192
+ # If auth is disabled, return None (will be handled as anonymous)
193
+ if not api_key_manager.auth_enabled:
194
+ return None
195
+
196
+ # Try Bearer token first
197
+ if bearer_token:
198
+ return bearer_token.credentials
199
+
200
+ # Try X-API-Key header
201
+ if header_key:
202
+ return header_key
203
+
204
+ # Try query parameter
205
+ if query_key:
206
+ return query_key
207
+
208
+ return None
209
+
210
+ async def authenticate_api_key(api_key: str = Depends(get_api_key_from_request)) -> Dict:
211
+ """Authenticate API key and return user info (optional when auth disabled)"""
212
+
213
+ # When auth is disabled, always succeed with anonymous user
214
+ if not api_key_manager.auth_enabled:
215
+ return {
216
+ "name": "anonymous",
217
+ "scopes": ["read", "write", "admin"],
218
+ "auth_enabled": False,
219
+ "authenticated": False
220
+ }
221
+
222
+ # When auth is enabled, require valid API key
223
+ if not api_key:
224
+ raise HTTPException(
225
+ status_code=status.HTTP_401_UNAUTHORIZED,
226
+ detail="API key required. Provide via Authorization header, X-API-Key header, or api_key query parameter",
227
+ headers={"WWW-Authenticate": "Bearer"}
228
+ )
229
+
230
+ key_data = api_key_manager.validate_api_key(api_key)
231
+
232
+ if not key_data:
233
+ raise HTTPException(
234
+ status_code=status.HTTP_401_UNAUTHORIZED,
235
+ detail="Invalid or expired API key",
236
+ headers={"WWW-Authenticate": "Bearer"}
237
+ )
238
+
239
+ key_data["authenticated"] = True
240
+ return key_data
241
+
242
+ async def require_scope(required_scope: str):
243
+ """Create a dependency that requires a specific scope"""
244
+ async def check_scope(current_user: Dict = Depends(authenticate_api_key)) -> Dict:
245
+ # When auth is disabled, always allow
246
+ if not current_user.get("auth_enabled", True):
247
+ return current_user
248
+
249
+ user_scopes = current_user.get("scopes", [])
250
+
251
+ if required_scope not in user_scopes and "admin" not in user_scopes:
252
+ raise HTTPException(
253
+ status_code=status.HTTP_403_FORBIDDEN,
254
+ detail=f"Insufficient permissions. Required scope: {required_scope}"
255
+ )
256
+
257
+ return current_user
258
+
259
+ return check_scope
260
+
261
+ # Convenience dependencies for common scopes
262
+ async def require_read_access(current_user: Dict = Depends(authenticate_api_key)) -> Dict:
263
+ """Require read access (or auth disabled)"""
264
+ if not current_user.get("auth_enabled", True):
265
+ return current_user
266
+
267
+ user_scopes = current_user.get("scopes", [])
268
+ if not any(scope in user_scopes for scope in ["read", "write", "admin"]):
269
+ raise HTTPException(
270
+ status_code=status.HTTP_403_FORBIDDEN,
271
+ detail="Read access required"
272
+ )
273
+ return current_user
274
+
275
+ async def require_write_access(current_user: Dict = Depends(authenticate_api_key)) -> Dict:
276
+ """Require write access (or auth disabled)"""
277
+ if not current_user.get("auth_enabled", True):
278
+ return current_user
279
+
280
+ user_scopes = current_user.get("scopes", [])
281
+ if not any(scope in user_scopes for scope in ["write", "admin"]):
282
+ raise HTTPException(
283
+ status_code=status.HTTP_403_FORBIDDEN,
284
+ detail="Write access required"
285
+ )
286
+ return current_user
287
+
288
+ async def require_admin_access(current_user: Dict = Depends(authenticate_api_key)) -> Dict:
289
+ """Require admin access (or auth disabled)"""
290
+ if not current_user.get("auth_enabled", True):
291
+ return current_user
292
+
293
+ user_scopes = current_user.get("scopes", [])
294
+ if "admin" not in user_scopes:
295
+ raise HTTPException(
296
+ status_code=status.HTTP_403_FORBIDDEN,
297
+ detail="Admin access required"
298
+ )
299
+ return current_user
300
+
301
+ # Optional authentication (always returns user info, never fails)
302
+ async def optional_auth(api_key: str = Depends(get_api_key_from_request)) -> Dict:
303
+ """Optional authentication - returns user info if available, anonymous if not"""
304
+ try:
305
+ return api_key_manager.validate_api_key(api_key) or {
306
+ "name": "anonymous",
307
+ "scopes": [],
308
+ "auth_enabled": api_key_manager.auth_enabled,
309
+ "authenticated": False
310
+ }
311
+ except Exception:
312
+ return {
313
+ "name": "anonymous",
314
+ "scopes": [],
315
+ "auth_enabled": api_key_manager.auth_enabled,
316
+ "authenticated": False
317
+ }
@@ -0,0 +1,268 @@
1
+ """
2
+ Security middleware for production deployment
3
+
4
+ Provides comprehensive security features including:
5
+ - Rate limiting with Redis backend
6
+ - Security headers
7
+ - Request size limits
8
+ - Input validation and sanitization
9
+ - CORS protection
10
+ """
11
+
12
+ import time
13
+ import logging
14
+ import os
15
+ import redis
16
+ from typing import Dict, Any, Optional, Callable
17
+ from fastapi import FastAPI, Request, Response, HTTPException, status
18
+ from fastapi.middleware.cors import CORSMiddleware
19
+ from fastapi.middleware.trustedhost import TrustedHostMiddleware
20
+ from slowapi import Limiter, _rate_limit_exceeded_handler
21
+ from slowapi.util import get_remote_address
22
+ from slowapi.errors import RateLimitExceeded
23
+ from starlette.middleware.base import BaseHTTPMiddleware
24
+ from starlette.responses import JSONResponse
25
+ import html
26
+
27
+ from ....core.config.config_manager import ConfigManager
28
+
29
+ # Configure logging
30
+ logger = logging.getLogger(__name__)
31
+
32
+ # Configuration from environment variables
33
+ config_manager = ConfigManager()
34
+ MAX_REQUEST_SIZE = int(os.getenv("MAX_REQUEST_SIZE_MB", "50")) * 1024 * 1024 # 50MB default
35
+ # Use Consul discovery for Redis URL with fallback
36
+ REDIS_URL = os.getenv("REDIS_URL", config_manager.get_redis_url())
37
+ ENABLE_RATE_LIMITING = os.getenv("ENABLE_RATE_LIMITING", "true").lower() == "true"
38
+ RATE_LIMIT_PER_MINUTE = os.getenv("RATE_LIMIT_PER_MINUTE", "100")
39
+ RATE_LIMIT_PER_HOUR = os.getenv("RATE_LIMIT_PER_HOUR", "1000")
40
+
41
+ # Security headers configuration
42
+ SECURITY_HEADERS = {
43
+ "X-Content-Type-Options": "nosniff",
44
+ "X-Frame-Options": "DENY",
45
+ "X-XSS-Protection": "1; mode=block",
46
+ "Strict-Transport-Security": "max-age=31536000; includeSubDomains",
47
+ "Content-Security-Policy": "default-src 'self'; script-src 'self' 'unsafe-inline' https://unpkg.com https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.jsdelivr.net; font-src 'self' https://fonts.gstatic.com; img-src 'self' https://fastapi.tiangolo.com data:; connect-src 'self'",
48
+ "Referrer-Policy": "strict-origin-when-cross-origin",
49
+ "Permissions-Policy": "geolocation=(), microphone=(), camera=()"
50
+ }
51
+
52
+ # Initialize Redis connection for rate limiting
53
+ try:
54
+ redis_client = redis.from_url(REDIS_URL, decode_responses=True)
55
+ redis_client.ping() # Test connection
56
+ logger.info("Redis connection established for rate limiting")
57
+ except Exception as e:
58
+ logger.warning(f"Redis connection failed, using in-memory rate limiting: {e}")
59
+ redis_client = None
60
+
61
+ # Initialize rate limiter
62
+ def get_remote_address_with_proxy(request: Request):
63
+ """Get client IP considering proxy headers"""
64
+ forwarded_for = request.headers.get("X-Forwarded-For")
65
+ if forwarded_for:
66
+ return forwarded_for.split(",")[0].strip()
67
+
68
+ real_ip = request.headers.get("X-Real-IP")
69
+ if real_ip:
70
+ return real_ip
71
+
72
+ return get_remote_address(request)
73
+
74
+ # Rate limiter with Redis backend if available
75
+ if redis_client:
76
+ limiter = Limiter(
77
+ key_func=get_remote_address_with_proxy,
78
+ storage_uri=REDIS_URL,
79
+ strategy="fixed-window"
80
+ )
81
+ else:
82
+ limiter = Limiter(
83
+ key_func=get_remote_address_with_proxy,
84
+ strategy="fixed-window"
85
+ )
86
+
87
+ class SecurityHeadersMiddleware(BaseHTTPMiddleware):
88
+ """Add security headers to all responses"""
89
+
90
+ async def dispatch(self, request: Request, call_next: Callable) -> Response:
91
+ try:
92
+ response = await call_next(request)
93
+
94
+ # Add security headers
95
+ for header, value in SECURITY_HEADERS.items():
96
+ response.headers[header] = value
97
+
98
+ # Add processing time header
99
+ if hasattr(request.state, 'start_time'):
100
+ process_time = time.time() - request.state.start_time
101
+ response.headers["X-Process-Time"] = str(process_time)
102
+
103
+ return response
104
+
105
+ except Exception as e:
106
+ logger.error(f"Error in security headers middleware: {e}")
107
+ return JSONResponse(
108
+ status_code=500,
109
+ content={"error": "Internal server error"},
110
+ headers=SECURITY_HEADERS
111
+ )
112
+
113
+ class RequestValidationMiddleware(BaseHTTPMiddleware):
114
+ """Validate request size and sanitize inputs"""
115
+
116
+ async def dispatch(self, request: Request, call_next: Callable) -> Response:
117
+ # Record start time for performance monitoring
118
+ request.state.start_time = time.time()
119
+
120
+ try:
121
+ # Check request size
122
+ content_length = request.headers.get("content-length")
123
+ if content_length and int(content_length) > MAX_REQUEST_SIZE:
124
+ logger.warning(
125
+ f"Request too large: {content_length} bytes > {MAX_REQUEST_SIZE} bytes "
126
+ f"from client {get_remote_address_with_proxy(request)}"
127
+ )
128
+ raise HTTPException(
129
+ status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
130
+ detail=f"Request too large. Maximum size: {MAX_REQUEST_SIZE // (1024*1024)}MB"
131
+ )
132
+
133
+ # Sanitize query parameters
134
+ if request.url.query:
135
+ sanitized_query = html.escape(request.url.query)
136
+ if sanitized_query != request.url.query:
137
+ logger.warning(
138
+ f"Potentially malicious query parameters detected from client {get_remote_address_with_proxy(request)}: "
139
+ f"'{request.url.query}' -> '{sanitized_query}'"
140
+ )
141
+
142
+ # Log request details for monitoring
143
+ logger.info(
144
+ f"Request received: {request.method} {request.url.path} "
145
+ f"from {get_remote_address_with_proxy(request)} "
146
+ f"(UA: {request.headers.get('user-agent', 'unknown')})"
147
+ )
148
+
149
+ response = await call_next(request)
150
+
151
+ # Log response details
152
+ process_time = time.time() - request.state.start_time
153
+ logger.info(
154
+ f"Request completed: {request.method} {request.url.path} -> {response.status_code} "
155
+ f"in {process_time:.3f}s from {get_remote_address_with_proxy(request)}"
156
+ )
157
+
158
+ return response
159
+
160
+ except HTTPException:
161
+ raise
162
+ except Exception as e:
163
+ logger.error(
164
+ f"Error in request validation middleware: {e} "
165
+ f"({request.method} {request.url.path} from {get_remote_address_with_proxy(request)})"
166
+ )
167
+ raise HTTPException(
168
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
169
+ detail="Internal server error"
170
+ )
171
+
172
+ def setup_security_middleware(app: FastAPI):
173
+ """Setup all security middleware for the FastAPI application"""
174
+
175
+ # Rate limiting setup
176
+ if ENABLE_RATE_LIMITING:
177
+ app.state.limiter = limiter
178
+ app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
179
+ logger.info(f"Rate limiting enabled (Redis backend: {redis_client is not None})")
180
+
181
+ # Trusted hosts (production should specify allowed hosts)
182
+ allowed_hosts = os.getenv("ALLOWED_HOSTS", "*").split(",")
183
+ if allowed_hosts != ["*"]:
184
+ app.add_middleware(TrustedHostMiddleware, allowed_hosts=allowed_hosts)
185
+ logger.info(f"Trusted hosts middleware enabled: {allowed_hosts}")
186
+
187
+ # CORS configuration
188
+ cors_origins = os.getenv("CORS_ORIGINS", "*").split(",")
189
+ app.add_middleware(
190
+ CORSMiddleware,
191
+ allow_origins=cors_origins,
192
+ allow_credentials=True,
193
+ allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
194
+ allow_headers=["*"],
195
+ expose_headers=["X-Process-Time"]
196
+ )
197
+ logger.info(f"CORS middleware enabled for origins: {cors_origins}")
198
+
199
+ # Custom security middleware
200
+ app.add_middleware(SecurityHeadersMiddleware)
201
+ app.add_middleware(RequestValidationMiddleware)
202
+
203
+ logger.info("Security middleware setup completed")
204
+
205
+ def get_rate_limiter():
206
+ """Get the configured rate limiter"""
207
+ return limiter
208
+
209
+ # Rate limiting decorators for different use cases
210
+ def rate_limit_standard():
211
+ """Standard rate limit for general API usage"""
212
+ return limiter.limit(f"{RATE_LIMIT_PER_MINUTE}/minute")
213
+
214
+ def rate_limit_heavy():
215
+ """Heavy rate limit for resource-intensive operations"""
216
+ heavy_limit = int(RATE_LIMIT_PER_MINUTE) // 5 # 20% of standard limit
217
+ return limiter.limit(f"{heavy_limit}/minute")
218
+
219
+ def rate_limit_auth():
220
+ """Strict rate limit for authentication endpoints"""
221
+ return limiter.limit("10/minute")
222
+
223
+ # Security utilities
224
+ def sanitize_input(text: str) -> str:
225
+ """Sanitize text input to prevent XSS attacks"""
226
+ if not isinstance(text, str):
227
+ return text
228
+ return html.escape(text)
229
+
230
+ def validate_api_key_format(api_key: str) -> bool:
231
+ """Validate API key format"""
232
+ if not isinstance(api_key, str):
233
+ return False
234
+
235
+ # Check if it starts with expected prefix
236
+ if not api_key.startswith("isa_"):
237
+ return False
238
+
239
+ # Check minimum length (should be > 20 characters)
240
+ if len(api_key) < 25:
241
+ return False
242
+
243
+ return True
244
+
245
+ def get_client_info(request: Request) -> Dict[str, Any]:
246
+ """Extract client information for logging and monitoring"""
247
+ return {
248
+ "ip": get_remote_address_with_proxy(request),
249
+ "user_agent": request.headers.get("user-agent", "unknown"),
250
+ "referer": request.headers.get("referer"),
251
+ "forwarded_for": request.headers.get("x-forwarded-for"),
252
+ "real_ip": request.headers.get("x-real-ip"),
253
+ "method": request.method,
254
+ "path": request.url.path,
255
+ "query": request.url.query
256
+ }
257
+
258
+ # Health check for Redis connection
259
+ async def check_redis_health() -> Dict[str, Any]:
260
+ """Check Redis connection health"""
261
+ if not redis_client:
262
+ return {"redis": "disabled", "status": "ok"}
263
+
264
+ try:
265
+ redis_client.ping()
266
+ return {"redis": "connected", "status": "ok"}
267
+ except Exception as e:
268
+ return {"redis": "error", "status": "error", "error": str(e)}