mcli-framework 7.0.0__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.
Potentially problematic release.
This version of mcli-framework might be problematic. Click here for more details.
- mcli/app/chat_cmd.py +42 -0
- mcli/app/commands_cmd.py +226 -0
- mcli/app/completion_cmd.py +216 -0
- mcli/app/completion_helpers.py +288 -0
- mcli/app/cron_test_cmd.py +697 -0
- mcli/app/logs_cmd.py +419 -0
- mcli/app/main.py +492 -0
- mcli/app/model/model.py +1060 -0
- mcli/app/model_cmd.py +227 -0
- mcli/app/redis_cmd.py +269 -0
- mcli/app/video/video.py +1114 -0
- mcli/app/visual_cmd.py +303 -0
- mcli/chat/chat.py +2409 -0
- mcli/chat/command_rag.py +514 -0
- mcli/chat/enhanced_chat.py +652 -0
- mcli/chat/system_controller.py +1010 -0
- mcli/chat/system_integration.py +1016 -0
- mcli/cli.py +25 -0
- mcli/config.toml +20 -0
- mcli/lib/api/api.py +586 -0
- mcli/lib/api/daemon_client.py +203 -0
- mcli/lib/api/daemon_client_local.py +44 -0
- mcli/lib/api/daemon_decorator.py +217 -0
- mcli/lib/api/mcli_decorators.py +1032 -0
- mcli/lib/auth/auth.py +85 -0
- mcli/lib/auth/aws_manager.py +85 -0
- mcli/lib/auth/azure_manager.py +91 -0
- mcli/lib/auth/credential_manager.py +192 -0
- mcli/lib/auth/gcp_manager.py +93 -0
- mcli/lib/auth/key_manager.py +117 -0
- mcli/lib/auth/mcli_manager.py +93 -0
- mcli/lib/auth/token_manager.py +75 -0
- mcli/lib/auth/token_util.py +1011 -0
- mcli/lib/config/config.py +47 -0
- mcli/lib/discovery/__init__.py +1 -0
- mcli/lib/discovery/command_discovery.py +274 -0
- mcli/lib/erd/erd.py +1345 -0
- mcli/lib/erd/generate_graph.py +453 -0
- mcli/lib/files/files.py +76 -0
- mcli/lib/fs/fs.py +109 -0
- mcli/lib/lib.py +29 -0
- mcli/lib/logger/logger.py +611 -0
- mcli/lib/performance/optimizer.py +409 -0
- mcli/lib/performance/rust_bridge.py +502 -0
- mcli/lib/performance/uvloop_config.py +154 -0
- mcli/lib/pickles/pickles.py +50 -0
- mcli/lib/search/cached_vectorizer.py +479 -0
- mcli/lib/services/data_pipeline.py +460 -0
- mcli/lib/services/lsh_client.py +441 -0
- mcli/lib/services/redis_service.py +387 -0
- mcli/lib/shell/shell.py +137 -0
- mcli/lib/toml/toml.py +33 -0
- mcli/lib/ui/styling.py +47 -0
- mcli/lib/ui/visual_effects.py +634 -0
- mcli/lib/watcher/watcher.py +185 -0
- mcli/ml/api/app.py +215 -0
- mcli/ml/api/middleware.py +224 -0
- mcli/ml/api/routers/admin_router.py +12 -0
- mcli/ml/api/routers/auth_router.py +244 -0
- mcli/ml/api/routers/backtest_router.py +12 -0
- mcli/ml/api/routers/data_router.py +12 -0
- mcli/ml/api/routers/model_router.py +302 -0
- mcli/ml/api/routers/monitoring_router.py +12 -0
- mcli/ml/api/routers/portfolio_router.py +12 -0
- mcli/ml/api/routers/prediction_router.py +267 -0
- mcli/ml/api/routers/trade_router.py +12 -0
- mcli/ml/api/routers/websocket_router.py +76 -0
- mcli/ml/api/schemas.py +64 -0
- mcli/ml/auth/auth_manager.py +425 -0
- mcli/ml/auth/models.py +154 -0
- mcli/ml/auth/permissions.py +302 -0
- mcli/ml/backtesting/backtest_engine.py +502 -0
- mcli/ml/backtesting/performance_metrics.py +393 -0
- mcli/ml/cache.py +400 -0
- mcli/ml/cli/main.py +398 -0
- mcli/ml/config/settings.py +394 -0
- mcli/ml/configs/dvc_config.py +230 -0
- mcli/ml/configs/mlflow_config.py +131 -0
- mcli/ml/configs/mlops_manager.py +293 -0
- mcli/ml/dashboard/app.py +532 -0
- mcli/ml/dashboard/app_integrated.py +738 -0
- mcli/ml/dashboard/app_supabase.py +560 -0
- mcli/ml/dashboard/app_training.py +615 -0
- mcli/ml/dashboard/cli.py +51 -0
- mcli/ml/data_ingestion/api_connectors.py +501 -0
- mcli/ml/data_ingestion/data_pipeline.py +567 -0
- mcli/ml/data_ingestion/stream_processor.py +512 -0
- mcli/ml/database/migrations/env.py +94 -0
- mcli/ml/database/models.py +667 -0
- mcli/ml/database/session.py +200 -0
- mcli/ml/experimentation/ab_testing.py +845 -0
- mcli/ml/features/ensemble_features.py +607 -0
- mcli/ml/features/political_features.py +676 -0
- mcli/ml/features/recommendation_engine.py +809 -0
- mcli/ml/features/stock_features.py +573 -0
- mcli/ml/features/test_feature_engineering.py +346 -0
- mcli/ml/logging.py +85 -0
- mcli/ml/mlops/data_versioning.py +518 -0
- mcli/ml/mlops/experiment_tracker.py +377 -0
- mcli/ml/mlops/model_serving.py +481 -0
- mcli/ml/mlops/pipeline_orchestrator.py +614 -0
- mcli/ml/models/base_models.py +324 -0
- mcli/ml/models/ensemble_models.py +675 -0
- mcli/ml/models/recommendation_models.py +474 -0
- mcli/ml/models/test_models.py +487 -0
- mcli/ml/monitoring/drift_detection.py +676 -0
- mcli/ml/monitoring/metrics.py +45 -0
- mcli/ml/optimization/portfolio_optimizer.py +834 -0
- mcli/ml/preprocessing/data_cleaners.py +451 -0
- mcli/ml/preprocessing/feature_extractors.py +491 -0
- mcli/ml/preprocessing/ml_pipeline.py +382 -0
- mcli/ml/preprocessing/politician_trading_preprocessor.py +569 -0
- mcli/ml/preprocessing/test_preprocessing.py +294 -0
- mcli/ml/scripts/populate_sample_data.py +200 -0
- mcli/ml/tasks.py +400 -0
- mcli/ml/tests/test_integration.py +429 -0
- mcli/ml/tests/test_training_dashboard.py +387 -0
- mcli/public/oi/oi.py +15 -0
- mcli/public/public.py +4 -0
- mcli/self/self_cmd.py +1246 -0
- mcli/workflow/daemon/api_daemon.py +800 -0
- mcli/workflow/daemon/async_command_database.py +681 -0
- mcli/workflow/daemon/async_process_manager.py +591 -0
- mcli/workflow/daemon/client.py +530 -0
- mcli/workflow/daemon/commands.py +1196 -0
- mcli/workflow/daemon/daemon.py +905 -0
- mcli/workflow/daemon/daemon_api.py +59 -0
- mcli/workflow/daemon/enhanced_daemon.py +571 -0
- mcli/workflow/daemon/process_cli.py +244 -0
- mcli/workflow/daemon/process_manager.py +439 -0
- mcli/workflow/daemon/test_daemon.py +275 -0
- mcli/workflow/dashboard/dashboard_cmd.py +113 -0
- mcli/workflow/docker/docker.py +0 -0
- mcli/workflow/file/file.py +100 -0
- mcli/workflow/gcloud/config.toml +21 -0
- mcli/workflow/gcloud/gcloud.py +58 -0
- mcli/workflow/git_commit/ai_service.py +328 -0
- mcli/workflow/git_commit/commands.py +430 -0
- mcli/workflow/lsh_integration.py +355 -0
- mcli/workflow/model_service/client.py +594 -0
- mcli/workflow/model_service/download_and_run_efficient_models.py +288 -0
- mcli/workflow/model_service/lightweight_embedder.py +397 -0
- mcli/workflow/model_service/lightweight_model_server.py +714 -0
- mcli/workflow/model_service/lightweight_test.py +241 -0
- mcli/workflow/model_service/model_service.py +1955 -0
- mcli/workflow/model_service/ollama_efficient_runner.py +425 -0
- mcli/workflow/model_service/pdf_processor.py +386 -0
- mcli/workflow/model_service/test_efficient_runner.py +234 -0
- mcli/workflow/model_service/test_example.py +315 -0
- mcli/workflow/model_service/test_integration.py +131 -0
- mcli/workflow/model_service/test_new_features.py +149 -0
- mcli/workflow/openai/openai.py +99 -0
- mcli/workflow/politician_trading/commands.py +1790 -0
- mcli/workflow/politician_trading/config.py +134 -0
- mcli/workflow/politician_trading/connectivity.py +490 -0
- mcli/workflow/politician_trading/data_sources.py +395 -0
- mcli/workflow/politician_trading/database.py +410 -0
- mcli/workflow/politician_trading/demo.py +248 -0
- mcli/workflow/politician_trading/models.py +165 -0
- mcli/workflow/politician_trading/monitoring.py +413 -0
- mcli/workflow/politician_trading/scrapers.py +966 -0
- mcli/workflow/politician_trading/scrapers_california.py +412 -0
- mcli/workflow/politician_trading/scrapers_eu.py +377 -0
- mcli/workflow/politician_trading/scrapers_uk.py +350 -0
- mcli/workflow/politician_trading/scrapers_us_states.py +438 -0
- mcli/workflow/politician_trading/supabase_functions.py +354 -0
- mcli/workflow/politician_trading/workflow.py +852 -0
- mcli/workflow/registry/registry.py +180 -0
- mcli/workflow/repo/repo.py +223 -0
- mcli/workflow/scheduler/commands.py +493 -0
- mcli/workflow/scheduler/cron_parser.py +238 -0
- mcli/workflow/scheduler/job.py +182 -0
- mcli/workflow/scheduler/monitor.py +139 -0
- mcli/workflow/scheduler/persistence.py +324 -0
- mcli/workflow/scheduler/scheduler.py +679 -0
- mcli/workflow/sync/sync_cmd.py +437 -0
- mcli/workflow/sync/test_cmd.py +314 -0
- mcli/workflow/videos/videos.py +242 -0
- mcli/workflow/wakatime/wakatime.py +11 -0
- mcli/workflow/workflow.py +37 -0
- mcli_framework-7.0.0.dist-info/METADATA +479 -0
- mcli_framework-7.0.0.dist-info/RECORD +186 -0
- mcli_framework-7.0.0.dist-info/WHEEL +5 -0
- mcli_framework-7.0.0.dist-info/entry_points.txt +7 -0
- mcli_framework-7.0.0.dist-info/licenses/LICENSE +21 -0
- mcli_framework-7.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
"""Authentication manager with JWT support"""
|
|
2
|
+
|
|
3
|
+
import secrets
|
|
4
|
+
from datetime import datetime, timedelta
|
|
5
|
+
from typing import Optional, Union, Dict, Any
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
import bcrypt
|
|
9
|
+
import jwt
|
|
10
|
+
from fastapi import Depends, HTTPException, status, Request
|
|
11
|
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
12
|
+
from sqlalchemy.orm import Session
|
|
13
|
+
from sqlalchemy import select
|
|
14
|
+
|
|
15
|
+
from mcli.ml.config import settings
|
|
16
|
+
from mcli.ml.database.models import User, UserRole
|
|
17
|
+
from mcli.ml.database.session import get_db
|
|
18
|
+
from .models import UserCreate, UserLogin, TokenResponse, TokenData, UserResponse
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Security scheme
|
|
22
|
+
security = HTTPBearer()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AuthManager:
|
|
26
|
+
"""Authentication and authorization manager"""
|
|
27
|
+
|
|
28
|
+
def __init__(self):
|
|
29
|
+
self.secret_key = settings.api.secret_key
|
|
30
|
+
self.algorithm = settings.api.algorithm
|
|
31
|
+
self.access_token_expire_minutes = settings.api.access_token_expire_minutes
|
|
32
|
+
self.refresh_token_expire_days = 7
|
|
33
|
+
|
|
34
|
+
def hash_password(self, password: str) -> str:
|
|
35
|
+
"""Hash a password using bcrypt"""
|
|
36
|
+
salt = bcrypt.gensalt()
|
|
37
|
+
hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
|
|
38
|
+
return hashed.decode('utf-8')
|
|
39
|
+
|
|
40
|
+
def verify_password(self, plain_password: str, hashed_password: str) -> bool:
|
|
41
|
+
"""Verify a password against a hash"""
|
|
42
|
+
return bcrypt.checkpw(
|
|
43
|
+
plain_password.encode('utf-8'),
|
|
44
|
+
hashed_password.encode('utf-8')
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def create_access_token(
|
|
48
|
+
self,
|
|
49
|
+
user_id: str,
|
|
50
|
+
username: str,
|
|
51
|
+
role: str,
|
|
52
|
+
expires_delta: Optional[timedelta] = None
|
|
53
|
+
) -> str:
|
|
54
|
+
"""Create a JWT access token"""
|
|
55
|
+
if expires_delta:
|
|
56
|
+
expire = datetime.utcnow() + expires_delta
|
|
57
|
+
else:
|
|
58
|
+
expire = datetime.utcnow() + timedelta(minutes=self.access_token_expire_minutes)
|
|
59
|
+
|
|
60
|
+
payload = {
|
|
61
|
+
"sub": user_id,
|
|
62
|
+
"username": username,
|
|
63
|
+
"role": role,
|
|
64
|
+
"exp": expire,
|
|
65
|
+
"iat": datetime.utcnow(),
|
|
66
|
+
"jti": secrets.token_urlsafe(32), # JWT ID for token revocation
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
encoded_jwt = jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
|
|
70
|
+
return encoded_jwt
|
|
71
|
+
|
|
72
|
+
def create_refresh_token(
|
|
73
|
+
self,
|
|
74
|
+
user_id: str,
|
|
75
|
+
expires_delta: Optional[timedelta] = None
|
|
76
|
+
) -> str:
|
|
77
|
+
"""Create a refresh token"""
|
|
78
|
+
if expires_delta:
|
|
79
|
+
expire = datetime.utcnow() + expires_delta
|
|
80
|
+
else:
|
|
81
|
+
expire = datetime.utcnow() + timedelta(days=self.refresh_token_expire_days)
|
|
82
|
+
|
|
83
|
+
payload = {
|
|
84
|
+
"sub": user_id,
|
|
85
|
+
"type": "refresh",
|
|
86
|
+
"exp": expire,
|
|
87
|
+
"iat": datetime.utcnow(),
|
|
88
|
+
"jti": secrets.token_urlsafe(32),
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
encoded_jwt = jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
|
|
92
|
+
return encoded_jwt
|
|
93
|
+
|
|
94
|
+
def verify_token(self, token: str) -> Optional[TokenData]:
|
|
95
|
+
"""Verify and decode a JWT token"""
|
|
96
|
+
try:
|
|
97
|
+
payload = jwt.decode(
|
|
98
|
+
token,
|
|
99
|
+
self.secret_key,
|
|
100
|
+
algorithms=[self.algorithm]
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
token_data = TokenData(
|
|
104
|
+
sub=payload.get("sub"),
|
|
105
|
+
username=payload.get("username"),
|
|
106
|
+
role=payload.get("role"),
|
|
107
|
+
exp=datetime.fromtimestamp(payload.get("exp")),
|
|
108
|
+
iat=datetime.fromtimestamp(payload.get("iat")),
|
|
109
|
+
jti=payload.get("jti"),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return token_data
|
|
113
|
+
|
|
114
|
+
except jwt.ExpiredSignatureError:
|
|
115
|
+
raise HTTPException(
|
|
116
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
117
|
+
detail="Token has expired",
|
|
118
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
119
|
+
)
|
|
120
|
+
except jwt.JWTError:
|
|
121
|
+
raise HTTPException(
|
|
122
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
123
|
+
detail="Could not validate credentials",
|
|
124
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
async def register_user(
|
|
128
|
+
self,
|
|
129
|
+
user_data: UserCreate,
|
|
130
|
+
db: Session
|
|
131
|
+
) -> User:
|
|
132
|
+
"""Register a new user"""
|
|
133
|
+
# Check if user already exists
|
|
134
|
+
existing_user = db.query(User).filter(
|
|
135
|
+
(User.username == user_data.username) |
|
|
136
|
+
(User.email == user_data.email)
|
|
137
|
+
).first()
|
|
138
|
+
|
|
139
|
+
if existing_user:
|
|
140
|
+
if existing_user.username == user_data.username:
|
|
141
|
+
raise HTTPException(
|
|
142
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
143
|
+
detail="Username already registered"
|
|
144
|
+
)
|
|
145
|
+
else:
|
|
146
|
+
raise HTTPException(
|
|
147
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
148
|
+
detail="Email already registered"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Create new user
|
|
152
|
+
hashed_password = self.hash_password(user_data.password)
|
|
153
|
+
|
|
154
|
+
new_user = User(
|
|
155
|
+
username=user_data.username,
|
|
156
|
+
email=user_data.email,
|
|
157
|
+
password_hash=hashed_password,
|
|
158
|
+
first_name=user_data.first_name,
|
|
159
|
+
last_name=user_data.last_name,
|
|
160
|
+
role=UserRole.USER,
|
|
161
|
+
is_active=True,
|
|
162
|
+
is_verified=False,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
db.add(new_user)
|
|
166
|
+
db.commit()
|
|
167
|
+
db.refresh(new_user)
|
|
168
|
+
|
|
169
|
+
return new_user
|
|
170
|
+
|
|
171
|
+
async def authenticate_user(
|
|
172
|
+
self,
|
|
173
|
+
login_data: UserLogin,
|
|
174
|
+
db: Session
|
|
175
|
+
) -> Optional[User]:
|
|
176
|
+
"""Authenticate a user"""
|
|
177
|
+
user = db.query(User).filter(
|
|
178
|
+
User.username == login_data.username
|
|
179
|
+
).first()
|
|
180
|
+
|
|
181
|
+
if not user:
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
if not self.verify_password(login_data.password, user.password_hash):
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
# Update last login
|
|
188
|
+
user.last_login_at = datetime.utcnow()
|
|
189
|
+
db.commit()
|
|
190
|
+
|
|
191
|
+
return user
|
|
192
|
+
|
|
193
|
+
async def login(
|
|
194
|
+
self,
|
|
195
|
+
login_data: UserLogin,
|
|
196
|
+
db: Session
|
|
197
|
+
) -> TokenResponse:
|
|
198
|
+
"""Login user and return tokens"""
|
|
199
|
+
user = await self.authenticate_user(login_data, db)
|
|
200
|
+
|
|
201
|
+
if not user:
|
|
202
|
+
raise HTTPException(
|
|
203
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
204
|
+
detail="Invalid username or password",
|
|
205
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
if not user.is_active:
|
|
209
|
+
raise HTTPException(
|
|
210
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
211
|
+
detail="User account is disabled"
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Create tokens
|
|
215
|
+
access_token = self.create_access_token(
|
|
216
|
+
user_id=str(user.id),
|
|
217
|
+
username=user.username,
|
|
218
|
+
role=user.role.value
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
refresh_token = self.create_refresh_token(user_id=str(user.id))
|
|
222
|
+
|
|
223
|
+
return TokenResponse(
|
|
224
|
+
access_token=access_token,
|
|
225
|
+
refresh_token=refresh_token,
|
|
226
|
+
expires_in=self.access_token_expire_minutes * 60,
|
|
227
|
+
user=UserResponse.from_orm(user)
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
async def refresh_access_token(
|
|
231
|
+
self,
|
|
232
|
+
refresh_token: str,
|
|
233
|
+
db: Session
|
|
234
|
+
) -> TokenResponse:
|
|
235
|
+
"""Refresh access token using refresh token"""
|
|
236
|
+
try:
|
|
237
|
+
payload = jwt.decode(
|
|
238
|
+
refresh_token,
|
|
239
|
+
self.secret_key,
|
|
240
|
+
algorithms=[self.algorithm]
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
if payload.get("type") != "refresh":
|
|
244
|
+
raise HTTPException(
|
|
245
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
246
|
+
detail="Invalid refresh token"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
user_id = payload.get("sub")
|
|
250
|
+
user = db.query(User).filter(User.id == user_id).first()
|
|
251
|
+
|
|
252
|
+
if not user or not user.is_active:
|
|
253
|
+
raise HTTPException(
|
|
254
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
255
|
+
detail="User not found or disabled"
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# Create new access token
|
|
259
|
+
access_token = self.create_access_token(
|
|
260
|
+
user_id=str(user.id),
|
|
261
|
+
username=user.username,
|
|
262
|
+
role=user.role.value
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
return TokenResponse(
|
|
266
|
+
access_token=access_token,
|
|
267
|
+
refresh_token=refresh_token, # Return same refresh token
|
|
268
|
+
expires_in=self.access_token_expire_minutes * 60,
|
|
269
|
+
user=UserResponse.from_orm(user)
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
except jwt.ExpiredSignatureError:
|
|
273
|
+
raise HTTPException(
|
|
274
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
275
|
+
detail="Refresh token has expired"
|
|
276
|
+
)
|
|
277
|
+
except jwt.JWTError:
|
|
278
|
+
raise HTTPException(
|
|
279
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
280
|
+
detail="Invalid refresh token"
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
async def get_current_user(
|
|
284
|
+
self,
|
|
285
|
+
credentials: HTTPAuthorizationCredentials = Depends(security),
|
|
286
|
+
db: Session = Depends(get_db)
|
|
287
|
+
) -> User:
|
|
288
|
+
"""Get current authenticated user from JWT token"""
|
|
289
|
+
token = credentials.credentials
|
|
290
|
+
|
|
291
|
+
token_data = self.verify_token(token)
|
|
292
|
+
|
|
293
|
+
user = db.query(User).filter(User.id == token_data.sub).first()
|
|
294
|
+
|
|
295
|
+
if not user:
|
|
296
|
+
raise HTTPException(
|
|
297
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
298
|
+
detail="User not found"
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
if not user.is_active:
|
|
302
|
+
raise HTTPException(
|
|
303
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
304
|
+
detail="User account is disabled"
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
return user
|
|
308
|
+
|
|
309
|
+
def require_role(self, *allowed_roles: UserRole):
|
|
310
|
+
"""Decorator/dependency to require specific roles"""
|
|
311
|
+
async def role_checker(
|
|
312
|
+
current_user: User = Depends(self.get_current_user)
|
|
313
|
+
) -> User:
|
|
314
|
+
if current_user.role not in allowed_roles:
|
|
315
|
+
raise HTTPException(
|
|
316
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
317
|
+
detail="Insufficient permissions"
|
|
318
|
+
)
|
|
319
|
+
return current_user
|
|
320
|
+
|
|
321
|
+
return role_checker
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# Global auth manager instance
|
|
325
|
+
auth_manager = AuthManager()
|
|
326
|
+
|
|
327
|
+
# Convenience functions
|
|
328
|
+
hash_password = auth_manager.hash_password
|
|
329
|
+
verify_password = auth_manager.verify_password
|
|
330
|
+
create_access_token = auth_manager.create_access_token
|
|
331
|
+
verify_access_token = auth_manager.verify_token
|
|
332
|
+
get_current_user = auth_manager.get_current_user
|
|
333
|
+
require_role = auth_manager.require_role
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
async def get_current_active_user(
|
|
337
|
+
current_user: User = Depends(get_current_user)
|
|
338
|
+
) -> User:
|
|
339
|
+
"""Get current active user"""
|
|
340
|
+
if not current_user.is_active:
|
|
341
|
+
raise HTTPException(
|
|
342
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
343
|
+
detail="Inactive user"
|
|
344
|
+
)
|
|
345
|
+
return current_user
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
async def get_admin_user(
|
|
349
|
+
current_user: User = Depends(get_current_user)
|
|
350
|
+
) -> User:
|
|
351
|
+
"""Get current admin user"""
|
|
352
|
+
if current_user.role != UserRole.ADMIN:
|
|
353
|
+
raise HTTPException(
|
|
354
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
355
|
+
detail="Admin access required"
|
|
356
|
+
)
|
|
357
|
+
return current_user
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
# Rate limiting
|
|
361
|
+
from collections import defaultdict
|
|
362
|
+
from datetime import datetime, timedelta
|
|
363
|
+
import asyncio
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
class RateLimiter:
|
|
367
|
+
"""Simple rate limiter"""
|
|
368
|
+
|
|
369
|
+
def __init__(self, requests: int = 100, window: int = 60):
|
|
370
|
+
self.requests = requests
|
|
371
|
+
self.window = window
|
|
372
|
+
self.clients = defaultdict(list)
|
|
373
|
+
self._cleanup_task = None
|
|
374
|
+
|
|
375
|
+
async def check_rate_limit(self, client_id: str) -> bool:
|
|
376
|
+
"""Check if client has exceeded rate limit"""
|
|
377
|
+
now = datetime.utcnow()
|
|
378
|
+
minute_ago = now - timedelta(seconds=self.window)
|
|
379
|
+
|
|
380
|
+
# Clean old requests
|
|
381
|
+
self.clients[client_id] = [
|
|
382
|
+
req_time for req_time in self.clients[client_id]
|
|
383
|
+
if req_time > minute_ago
|
|
384
|
+
]
|
|
385
|
+
|
|
386
|
+
# Check limit
|
|
387
|
+
if len(self.clients[client_id]) >= self.requests:
|
|
388
|
+
return False
|
|
389
|
+
|
|
390
|
+
# Add current request
|
|
391
|
+
self.clients[client_id].append(now)
|
|
392
|
+
return True
|
|
393
|
+
|
|
394
|
+
async def cleanup(self):
|
|
395
|
+
"""Periodic cleanup of old entries"""
|
|
396
|
+
while True:
|
|
397
|
+
await asyncio.sleep(300) # Clean every 5 minutes
|
|
398
|
+
now = datetime.utcnow()
|
|
399
|
+
window_start = now - timedelta(seconds=self.window)
|
|
400
|
+
|
|
401
|
+
for client_id in list(self.clients.keys()):
|
|
402
|
+
self.clients[client_id] = [
|
|
403
|
+
req_time for req_time in self.clients[client_id]
|
|
404
|
+
if req_time > window_start
|
|
405
|
+
]
|
|
406
|
+
|
|
407
|
+
if not self.clients[client_id]:
|
|
408
|
+
del self.clients[client_id]
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
# Global rate limiter
|
|
412
|
+
rate_limiter = RateLimiter(requests=settings.api.rate_limit, window=60)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
async def check_rate_limit(request: Request):
|
|
416
|
+
"""FastAPI dependency to check rate limit"""
|
|
417
|
+
client_ip = request.client.host
|
|
418
|
+
|
|
419
|
+
if not await rate_limiter.check_rate_limit(client_ip):
|
|
420
|
+
raise HTTPException(
|
|
421
|
+
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
|
422
|
+
detail="Rate limit exceeded"
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
return True
|
mcli/ml/auth/models.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Authentication data models"""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Optional, List
|
|
5
|
+
from pydantic import BaseModel, EmailStr, Field, validator
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class UserCreate(BaseModel):
|
|
10
|
+
"""User registration model"""
|
|
11
|
+
username: str = Field(..., min_length=3, max_length=50, regex="^[a-zA-Z0-9_-]+$")
|
|
12
|
+
email: EmailStr
|
|
13
|
+
password: str = Field(..., min_length=8, max_length=100)
|
|
14
|
+
first_name: Optional[str] = Field(None, max_length=50)
|
|
15
|
+
last_name: Optional[str] = Field(None, max_length=50)
|
|
16
|
+
|
|
17
|
+
@validator('password')
|
|
18
|
+
def validate_password(cls, v):
|
|
19
|
+
"""Ensure password meets security requirements"""
|
|
20
|
+
if len(v) < 8:
|
|
21
|
+
raise ValueError('Password must be at least 8 characters long')
|
|
22
|
+
if not any(char.isdigit() for char in v):
|
|
23
|
+
raise ValueError('Password must contain at least one digit')
|
|
24
|
+
if not any(char.isupper() for char in v):
|
|
25
|
+
raise ValueError('Password must contain at least one uppercase letter')
|
|
26
|
+
if not any(char.islower() for char in v):
|
|
27
|
+
raise ValueError('Password must contain at least one lowercase letter')
|
|
28
|
+
return v
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class UserLogin(BaseModel):
|
|
32
|
+
"""User login model"""
|
|
33
|
+
username: str
|
|
34
|
+
password: str
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class UserResponse(BaseModel):
|
|
38
|
+
"""User response model"""
|
|
39
|
+
id: UUID
|
|
40
|
+
username: str
|
|
41
|
+
email: str
|
|
42
|
+
first_name: Optional[str]
|
|
43
|
+
last_name: Optional[str]
|
|
44
|
+
role: str
|
|
45
|
+
is_active: bool
|
|
46
|
+
is_verified: bool
|
|
47
|
+
created_at: datetime
|
|
48
|
+
last_login_at: Optional[datetime]
|
|
49
|
+
|
|
50
|
+
class Config:
|
|
51
|
+
orm_mode = True
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class TokenResponse(BaseModel):
|
|
55
|
+
"""JWT token response"""
|
|
56
|
+
access_token: str
|
|
57
|
+
token_type: str = "Bearer"
|
|
58
|
+
expires_in: int
|
|
59
|
+
refresh_token: Optional[str] = None
|
|
60
|
+
user: UserResponse
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class TokenData(BaseModel):
|
|
64
|
+
"""JWT token payload"""
|
|
65
|
+
sub: str # User ID
|
|
66
|
+
username: str
|
|
67
|
+
role: str
|
|
68
|
+
exp: datetime
|
|
69
|
+
iat: datetime
|
|
70
|
+
jti: Optional[str] = None # JWT ID for token revocation
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class PasswordReset(BaseModel):
|
|
74
|
+
"""Password reset request"""
|
|
75
|
+
email: EmailStr
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class PasswordResetConfirm(BaseModel):
|
|
79
|
+
"""Password reset confirmation"""
|
|
80
|
+
token: str
|
|
81
|
+
new_password: str = Field(..., min_length=8, max_length=100)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class PasswordChange(BaseModel):
|
|
85
|
+
"""Password change request"""
|
|
86
|
+
current_password: str
|
|
87
|
+
new_password: str = Field(..., min_length=8, max_length=100)
|
|
88
|
+
|
|
89
|
+
@validator('new_password')
|
|
90
|
+
def validate_password(cls, v, values):
|
|
91
|
+
"""Ensure new password is different and meets requirements"""
|
|
92
|
+
if 'current_password' in values and v == values['current_password']:
|
|
93
|
+
raise ValueError('New password must be different from current password')
|
|
94
|
+
|
|
95
|
+
if len(v) < 8:
|
|
96
|
+
raise ValueError('Password must be at least 8 characters long')
|
|
97
|
+
if not any(char.isdigit() for char in v):
|
|
98
|
+
raise ValueError('Password must contain at least one digit')
|
|
99
|
+
if not any(char.isupper() for char in v):
|
|
100
|
+
raise ValueError('Password must contain at least one uppercase letter')
|
|
101
|
+
if not any(char.islower() for char in v):
|
|
102
|
+
raise ValueError('Password must contain at least one lowercase letter')
|
|
103
|
+
return v
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class APIKeyCreate(BaseModel):
|
|
107
|
+
"""API key creation model"""
|
|
108
|
+
name: str = Field(..., min_length=1, max_length=100)
|
|
109
|
+
expires_at: Optional[datetime] = None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class APIKeyResponse(BaseModel):
|
|
113
|
+
"""API key response"""
|
|
114
|
+
key: str
|
|
115
|
+
name: str
|
|
116
|
+
created_at: datetime
|
|
117
|
+
expires_at: Optional[datetime]
|
|
118
|
+
last_used_at: Optional[datetime]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class UserUpdate(BaseModel):
|
|
122
|
+
"""User update model"""
|
|
123
|
+
email: Optional[EmailStr] = None
|
|
124
|
+
first_name: Optional[str] = Field(None, max_length=50)
|
|
125
|
+
last_name: Optional[str] = Field(None, max_length=50)
|
|
126
|
+
is_active: Optional[bool] = None
|
|
127
|
+
role: Optional[str] = None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class UserPermissions(BaseModel):
|
|
131
|
+
"""User permissions model"""
|
|
132
|
+
user_id: UUID
|
|
133
|
+
permissions: List[str]
|
|
134
|
+
roles: List[str]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class SessionInfo(BaseModel):
|
|
138
|
+
"""Session information"""
|
|
139
|
+
session_id: str
|
|
140
|
+
user_id: UUID
|
|
141
|
+
ip_address: str
|
|
142
|
+
user_agent: str
|
|
143
|
+
created_at: datetime
|
|
144
|
+
expires_at: datetime
|
|
145
|
+
is_active: bool
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class LoginAttempt(BaseModel):
|
|
149
|
+
"""Login attempt tracking"""
|
|
150
|
+
username: str
|
|
151
|
+
ip_address: str
|
|
152
|
+
success: bool
|
|
153
|
+
timestamp: datetime
|
|
154
|
+
failure_reason: Optional[str] = None
|