Fix code formatting and linting issues
- Auto-format all Python files with black and isort - Remove unused imports with autoflake - Fix flake8 issues (missing newlines, blank lines, etc.) - Fix nonlocal/global scope issues in main.py - Fix security.py import order (E402) - Remove f-string without placeholders - Add nosec comment for intentional exception handling - Fix test imports to match refactored DMARCParser API Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
+19
-19
@@ -1,12 +1,12 @@
|
||||
from functools import lru_cache
|
||||
from typing import Optional, List, Union
|
||||
import secrets
|
||||
import logging
|
||||
import secrets
|
||||
from functools import lru_cache
|
||||
from typing import List, Optional, Union
|
||||
|
||||
# Try to import from pydantic_settings first (newer versions)
|
||||
try:
|
||||
from pydantic_settings import BaseSettings
|
||||
from pydantic import EmailStr, validator
|
||||
from pydantic_settings import BaseSettings
|
||||
except ImportError:
|
||||
# Fall back to older pydantic version
|
||||
from pydantic import BaseSettings, EmailStr, validator
|
||||
@@ -16,42 +16,42 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""Application settings"""
|
||||
|
||||
|
||||
# Base
|
||||
PROJECT_NAME: str = "DMARQ"
|
||||
API_V1_STR: str = "/api/v1"
|
||||
|
||||
|
||||
# Database
|
||||
DATABASE_URL: str = "sqlite:///./dmarq.db"
|
||||
|
||||
|
||||
# JWT Authentication
|
||||
SECRET_KEY: Optional[str] = None
|
||||
ALGORITHM: str = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 # 1 hour
|
||||
|
||||
|
||||
# CORS
|
||||
BACKEND_CORS_ORIGINS: List[str] = ["http://localhost:3000", "http://localhost:5173"]
|
||||
|
||||
|
||||
# IMAP Settings
|
||||
IMAP_SERVER: Optional[str] = None
|
||||
IMAP_PORT: int = 993
|
||||
IMAP_USERNAME: Optional[str] = None
|
||||
IMAP_PASSWORD: Optional[str] = None
|
||||
|
||||
|
||||
# Admin User
|
||||
FIRST_SUPERUSER: Optional[EmailStr] = None
|
||||
FIRST_SUPERUSER_PASSWORD: Optional[str] = None
|
||||
|
||||
|
||||
# Optional Cloudflare Integration
|
||||
CLOUDFLARE_API_TOKEN: Optional[str] = None
|
||||
CLOUDFLARE_ZONE_ID: Optional[str] = None
|
||||
|
||||
|
||||
@validator("SECRET_KEY", pre=True, always=True)
|
||||
def validate_secret_key(cls, v: Optional[str]) -> str:
|
||||
"""Validate and generate SECRET_KEY if not provided."""
|
||||
# Default insecure key that should never be used
|
||||
DEFAULT_INSECURE_KEY = "CHANGE_THIS_TO_A_RANDOM_SECRET_IN_PRODUCTION"
|
||||
|
||||
|
||||
if v is None or v == "" or v == DEFAULT_INSECURE_KEY:
|
||||
# Generate a secure random key
|
||||
generated_key = secrets.token_hex(32)
|
||||
@@ -59,19 +59,19 @@ class Settings(BaseSettings):
|
||||
"SECRET_KEY not configured or using default value! "
|
||||
"Generated a random key for this session. "
|
||||
"For production, set SECRET_KEY in your .env file using: "
|
||||
f"openssl rand -hex 32"
|
||||
"openssl rand -hex 32"
|
||||
)
|
||||
return generated_key
|
||||
|
||||
|
||||
# Check if key is too short
|
||||
if len(v) < 32:
|
||||
logger.warning(
|
||||
f"SECRET_KEY is too short ({len(v)} characters). "
|
||||
"Recommended minimum is 32 characters for security."
|
||||
)
|
||||
|
||||
|
||||
return v
|
||||
|
||||
|
||||
@validator("BACKEND_CORS_ORIGINS", pre=True)
|
||||
def assemble_cors_origins(cls, v: Union[str, List[str]]) -> List[str]:
|
||||
if isinstance(v, str) and not v.startswith("["):
|
||||
@@ -79,7 +79,7 @@ class Settings(BaseSettings):
|
||||
elif isinstance(v, (list, str)):
|
||||
return v
|
||||
raise ValueError(v)
|
||||
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
case_sensitive = True
|
||||
@@ -90,4 +90,4 @@ def get_settings() -> Settings:
|
||||
"""
|
||||
Get application settings from environment variables or .env file
|
||||
"""
|
||||
return Settings()
|
||||
return Settings()
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
from typing import Generator
|
||||
|
||||
from app.core.config import get_settings
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from app.core.config import get_settings
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
# Configure SQLAlchemy
|
||||
@@ -24,4 +23,4 @@ def get_db() -> Generator:
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
db.close()
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Union, Optional
|
||||
import secrets
|
||||
import logging
|
||||
|
||||
from fastapi import HTTPException, Security, status
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials, APIKeyHeader
|
||||
from jose import jwt, JWTError
|
||||
from passlib.context import CryptContext
|
||||
import os
|
||||
import secrets
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
from app.core.config import get_settings
|
||||
from fastapi import HTTPException, Security, status
|
||||
from fastapi.security import APIKeyHeader, HTTPAuthorizationCredentials, HTTPBearer
|
||||
from jose import JWTError, jwt
|
||||
from passlib.context import CryptContext
|
||||
|
||||
settings = get_settings()
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -24,7 +24,7 @@ api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
|
||||
# - Development and testing environments
|
||||
# - Single-instance deployments
|
||||
# - MVP/prototype applications
|
||||
#
|
||||
#
|
||||
# ⚠️ NOT SUITABLE FOR PRODUCTION when:
|
||||
# - Running multiple application instances (keys not shared)
|
||||
# - Requiring key persistence across restarts
|
||||
@@ -44,7 +44,6 @@ logger.warning(
|
||||
)
|
||||
|
||||
# Check if running in production mode and warn
|
||||
import os
|
||||
if os.getenv("ENVIRONMENT", "development").lower() == "production":
|
||||
logger.error(
|
||||
"CRITICAL: Running in PRODUCTION mode with in-memory API key storage! "
|
||||
@@ -56,7 +55,7 @@ if os.getenv("ENVIRONMENT", "development").lower() == "production":
|
||||
def generate_api_key() -> str:
|
||||
"""
|
||||
Generate a secure random API key.
|
||||
|
||||
|
||||
Returns:
|
||||
A 32-character hexadecimal API key
|
||||
"""
|
||||
@@ -66,10 +65,10 @@ def generate_api_key() -> str:
|
||||
def add_api_key(api_key: str) -> bool:
|
||||
"""
|
||||
Add an API key to the valid keys set.
|
||||
|
||||
|
||||
Args:
|
||||
api_key: The API key to add
|
||||
|
||||
|
||||
Returns:
|
||||
True if key was added, False if it already existed
|
||||
"""
|
||||
@@ -83,28 +82,26 @@ def add_api_key(api_key: str) -> bool:
|
||||
def verify_api_key(api_key: str) -> bool:
|
||||
"""
|
||||
Verify an API key is valid.
|
||||
|
||||
|
||||
Args:
|
||||
api_key: The API key to verify
|
||||
|
||||
|
||||
Returns:
|
||||
True if key is valid, False otherwise
|
||||
"""
|
||||
return api_key in _api_keys
|
||||
|
||||
|
||||
async def get_api_key(
|
||||
api_key_header: Optional[str] = Security(api_key_header)
|
||||
) -> str:
|
||||
async def get_api_key(api_key_header: Optional[str] = Security(api_key_header)) -> str:
|
||||
"""
|
||||
Dependency to verify API key authentication.
|
||||
|
||||
|
||||
Args:
|
||||
api_key_header: API key from X-API-Key header
|
||||
|
||||
|
||||
Returns:
|
||||
The validated API key
|
||||
|
||||
|
||||
Raises:
|
||||
HTTPException: If API key is missing or invalid
|
||||
"""
|
||||
@@ -114,30 +111,32 @@ async def get_api_key(
|
||||
detail="Missing API key",
|
||||
headers={"WWW-Authenticate": "ApiKey"},
|
||||
)
|
||||
|
||||
|
||||
if not verify_api_key(api_key_header):
|
||||
logger.warning(f"Invalid API key attempt: ...{api_key_header[-8:] if len(api_key_header) >= 8 else 'invalid'}")
|
||||
logger.warning(
|
||||
f"Invalid API key attempt: ...{api_key_header[-8:] if len(api_key_header) >= 8 else 'invalid'}"
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid API key",
|
||||
headers={"WWW-Authenticate": "ApiKey"},
|
||||
)
|
||||
|
||||
|
||||
return api_key_header
|
||||
|
||||
|
||||
async def verify_token(
|
||||
credentials: Optional[HTTPAuthorizationCredentials] = Security(security_bearer)
|
||||
credentials: Optional[HTTPAuthorizationCredentials] = Security(security_bearer),
|
||||
) -> dict:
|
||||
"""
|
||||
Dependency to verify JWT token authentication.
|
||||
|
||||
|
||||
Args:
|
||||
credentials: Bearer token from Authorization header
|
||||
|
||||
|
||||
Returns:
|
||||
Decoded token payload
|
||||
|
||||
|
||||
Raises:
|
||||
HTTPException: If token is missing or invalid
|
||||
"""
|
||||
@@ -147,9 +146,9 @@ async def verify_token(
|
||||
detail="Missing authentication token",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
|
||||
token = credentials.credentials
|
||||
|
||||
|
||||
try:
|
||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||
return payload
|
||||
@@ -164,39 +163,37 @@ async def verify_token(
|
||||
|
||||
async def require_admin_auth(
|
||||
api_key: Optional[str] = Security(api_key_header),
|
||||
bearer: Optional[HTTPAuthorizationCredentials] = Security(security_bearer)
|
||||
bearer: Optional[HTTPAuthorizationCredentials] = Security(security_bearer),
|
||||
) -> dict:
|
||||
"""
|
||||
Dependency to require either API key or JWT token authentication for admin endpoints.
|
||||
|
||||
|
||||
Checks API key first, then falls back to JWT token.
|
||||
|
||||
|
||||
Args:
|
||||
api_key: Optional API key from X-API-Key header
|
||||
bearer: Optional JWT token from Authorization header
|
||||
|
||||
|
||||
Returns:
|
||||
Authentication context (api_key or token payload)
|
||||
|
||||
|
||||
Raises:
|
||||
HTTPException: If no valid authentication is provided
|
||||
"""
|
||||
# Try API key first
|
||||
if api_key and verify_api_key(api_key):
|
||||
return {"auth_type": "api_key", "api_key": api_key}
|
||||
|
||||
|
||||
# Try JWT token
|
||||
if bearer:
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
bearer.credentials,
|
||||
settings.SECRET_KEY,
|
||||
algorithms=[settings.ALGORITHM]
|
||||
bearer.credentials, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
|
||||
)
|
||||
return {"auth_type": "jwt", "payload": payload}
|
||||
except JWTError as e:
|
||||
logger.warning(f"Invalid JWT token: {str(e)}")
|
||||
|
||||
|
||||
# No valid authentication provided
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
@@ -205,9 +202,7 @@ async def require_admin_auth(
|
||||
)
|
||||
|
||||
|
||||
def create_access_token(
|
||||
subject: Union[str, Any], expires_delta: timedelta = None
|
||||
) -> str:
|
||||
def create_access_token(subject: Union[str, Any], expires_delta: timedelta = None) -> str:
|
||||
"""
|
||||
Create a JWT access token for authentication
|
||||
"""
|
||||
@@ -231,4 +226,4 @@ def get_password_hash(password: str) -> str:
|
||||
"""
|
||||
Hash a password
|
||||
"""
|
||||
return pwd_context.hash(password)
|
||||
return pwd_context.hash(password)
|
||||
|
||||
Reference in New Issue
Block a user