updates
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -3,6 +3,7 @@ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from jose import JWTError, jwt
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
import os
|
||||
from ...shared.config.database import get_db
|
||||
from ...shared.config.settings import settings
|
||||
@@ -15,26 +16,39 @@ def get_jwt_secret() -> str:
|
||||
Get JWT secret securely, fail if not configured.
|
||||
Never use hardcoded fallback secrets.
|
||||
"""
|
||||
default_secret = 'dev-secret-key-change-in-production-12345'
|
||||
# Remove default secret entirely - fail fast if not configured
|
||||
jwt_secret = getattr(settings, 'JWT_SECRET', None) or os.getenv('JWT_SECRET', None)
|
||||
|
||||
# Fail fast if secret is not configured or using default value
|
||||
if not jwt_secret or jwt_secret == default_secret:
|
||||
if settings.is_production:
|
||||
raise ValueError(
|
||||
'CRITICAL: JWT_SECRET is not properly configured in production. '
|
||||
'Please set JWT_SECRET environment variable to a secure random string.'
|
||||
)
|
||||
# In development, warn but allow (startup validation should catch this)
|
||||
import warnings
|
||||
warnings.warn(
|
||||
f'JWT_SECRET not configured. Using settings value but this is insecure. '
|
||||
f'Set JWT_SECRET environment variable.',
|
||||
UserWarning
|
||||
# Fail fast if secret is not configured
|
||||
if not jwt_secret:
|
||||
error_msg = (
|
||||
'CRITICAL: JWT_SECRET is not configured. '
|
||||
'Please set JWT_SECRET environment variable to a secure random string (minimum 32 characters).'
|
||||
)
|
||||
jwt_secret = getattr(settings, 'JWT_SECRET', None)
|
||||
if not jwt_secret:
|
||||
raise ValueError('JWT_SECRET must be configured')
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.error(error_msg)
|
||||
if settings.is_production:
|
||||
raise ValueError(error_msg)
|
||||
else:
|
||||
# In development, generate a secure secret but warn
|
||||
import secrets
|
||||
jwt_secret = secrets.token_urlsafe(64)
|
||||
logger.warning(
|
||||
f'JWT_SECRET not configured. Auto-generated secret for development. '
|
||||
f'Set JWT_SECRET environment variable for production: {jwt_secret}'
|
||||
)
|
||||
|
||||
# Validate JWT secret strength
|
||||
if len(jwt_secret) < 32:
|
||||
error_msg = 'JWT_SECRET must be at least 32 characters long for security.'
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.error(error_msg)
|
||||
if settings.is_production:
|
||||
raise ValueError(error_msg)
|
||||
else:
|
||||
logger.warning(error_msg)
|
||||
|
||||
return jwt_secret
|
||||
|
||||
@@ -80,6 +94,22 @@ def get_current_user(
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
|
||||
# Check if user account is active
|
||||
if not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail='Account is disabled. Please contact support.'
|
||||
)
|
||||
|
||||
# Check if account is locked
|
||||
if user.locked_until and user.locked_until > datetime.utcnow():
|
||||
remaining_minutes = int((user.locked_until - datetime.utcnow()).total_seconds() / 60)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f'Account is temporarily locked due to multiple failed login attempts. Please try again in {remaining_minutes} minute(s).'
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
def authorize_roles(*allowed_roles: str):
|
||||
@@ -102,7 +132,8 @@ def get_current_user_optional(
|
||||
) -> Optional[User]:
|
||||
"""
|
||||
Get current user optionally from either Authorization header or httpOnly cookie.
|
||||
Returns None if no valid token is found.
|
||||
Returns None if no valid token is found, or if user is inactive/locked.
|
||||
This ensures inactive/locked users are never considered "authenticated" even for optional features.
|
||||
"""
|
||||
# Try to get token from Authorization header first
|
||||
token = None
|
||||
@@ -124,7 +155,19 @@ def get_current_user_optional(
|
||||
return None
|
||||
except (JWTError, ValueError):
|
||||
return None
|
||||
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if user is None:
|
||||
return None
|
||||
|
||||
# Check if user account is active - return None for inactive users
|
||||
if not user.is_active:
|
||||
return None
|
||||
|
||||
# Check if account is locked - return None for locked users
|
||||
if user.locked_until and user.locked_until > datetime.utcnow():
|
||||
return None
|
||||
|
||||
return user
|
||||
|
||||
def verify_token(token: str) -> dict:
|
||||
|
||||
47
Backend/src/security/middleware/role_based_rate_limit.py
Normal file
47
Backend/src/security/middleware/role_based_rate_limit.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""
|
||||
Role-based rate limiting middleware.
|
||||
"""
|
||||
from slowapi import Limiter
|
||||
from slowapi.util import get_remote_address
|
||||
from typing import Callable
|
||||
from fastapi import Request
|
||||
from ...shared.config.settings import settings
|
||||
from ...shared.config.logging_config import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
def get_rate_limit_key(request: Request) -> str:
|
||||
"""Get rate limit key based on user role if authenticated, otherwise IP."""
|
||||
# Try to get user from request state (set by auth middleware)
|
||||
if hasattr(request.state, 'user') and request.state.user:
|
||||
user = request.state.user
|
||||
# Get user role
|
||||
role = getattr(user, 'role', None)
|
||||
if role:
|
||||
role_name = getattr(role, 'name', None)
|
||||
if role_name:
|
||||
# Return role-based key
|
||||
return f"rate_limit:{role_name}:{user.id}"
|
||||
|
||||
# Fall back to IP-based limiting
|
||||
return get_remote_address(request)
|
||||
|
||||
def get_rate_limit_for_role(role_name: str) -> str:
|
||||
"""Get rate limit string for a specific role."""
|
||||
limits = {
|
||||
'admin': settings.RATE_LIMIT_ADMIN_PER_MINUTE,
|
||||
'staff': settings.RATE_LIMIT_STAFF_PER_MINUTE,
|
||||
'accountant': settings.RATE_LIMIT_ACCOUNTANT_PER_MINUTE,
|
||||
'customer': settings.RATE_LIMIT_CUSTOMER_PER_MINUTE,
|
||||
}
|
||||
|
||||
limit = limits.get(role_name, settings.RATE_LIMIT_PER_MINUTE)
|
||||
return f"{limit}/minute"
|
||||
|
||||
def create_role_based_limiter() -> Limiter:
|
||||
"""Create a role-based rate limiter."""
|
||||
return Limiter(
|
||||
key_func=get_rate_limit_key,
|
||||
default_limits=[f'{settings.RATE_LIMIT_PER_MINUTE}/minute']
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user