Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,31 @@ AUTH_SECRET=CHANGE-ME-IN-PRODUCTION
# Target repository in format "owner/repo"
# Example: frankbria/codeframe
# GITHUB_REPO=owner/repo

# ============================================================================
# Rate Limiting Configuration
# ============================================================================

# Enable/disable rate limiting (default: true)
# Set to false to disable rate limiting entirely
RATE_LIMIT_ENABLED=true

# Rate limit for authentication endpoints (default: 10/minute)
# Format: "N/period" where period is second, minute, hour, or day
RATE_LIMIT_AUTH=10/minute

# Rate limit for standard API endpoints (default: 100/minute)
RATE_LIMIT_STANDARD=100/minute

# Rate limit for AI/expensive operations like chat (default: 20/minute)
RATE_LIMIT_AI=20/minute

# Rate limit for WebSocket connections (default: 30/minute)
RATE_LIMIT_WEBSOCKET=30/minute

# Storage backend for rate limiting (default: memory)
# Options: memory (single instance) or redis (distributed)
RATE_LIMIT_STORAGE=memory

# Redis URL for distributed rate limiting (required if RATE_LIMIT_STORAGE=redis)
# REDIS_URL=redis://localhost:6379/0
133 changes: 133 additions & 0 deletions codeframe/config/rate_limits.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
"""Rate limiting configuration for CodeFRAME API.

This module provides configuration for API rate limiting using slowapi.
Rate limits can be configured via environment variables for flexibility
across deployment environments.

Environment Variables:
RATE_LIMIT_ENABLED: Enable/disable rate limiting (default: true)
RATE_LIMIT_AUTH: Rate limit for authentication endpoints (default: 10/minute)
RATE_LIMIT_STANDARD: Rate limit for standard API endpoints (default: 100/minute)
RATE_LIMIT_AI: Rate limit for AI/expensive operations (default: 20/minute)
RATE_LIMIT_WEBSOCKET: Rate limit for WebSocket connections (default: 30/minute)
RATE_LIMIT_STORAGE: Storage backend - memory or redis (default: memory)
REDIS_URL: Redis connection URL for distributed rate limiting (optional)
"""

import logging
import os
from dataclasses import dataclass
from typing import Optional

logger = logging.getLogger(__name__)


def _parse_bool(value: str) -> bool:
"""Parse boolean from string, supporting various formats.

Args:
value: String value to parse

Returns:
Boolean interpretation of the value
"""
return value.lower() in ("true", "1", "yes", "on")


@dataclass
class RateLimitConfig:
"""Configuration for API rate limiting.

Attributes:
auth_limit: Rate limit for authentication endpoints
standard_limit: Rate limit for standard API endpoints
ai_limit: Rate limit for AI/expensive operations
websocket_limit: Rate limit for WebSocket connections
enabled: Whether rate limiting is enabled
storage: Storage backend ('memory' or 'redis')
redis_url: Redis connection URL for distributed rate limiting
"""

auth_limit: str = "10/minute"
standard_limit: str = "100/minute"
ai_limit: str = "20/minute"
websocket_limit: str = "30/minute"
enabled: bool = True
storage: str = "memory"
redis_url: Optional[str] = None

@classmethod
def from_environment(cls) -> "RateLimitConfig":
"""Create RateLimitConfig from environment variables.

Returns:
RateLimitConfig instance with values from environment
"""
enabled_str = os.getenv("RATE_LIMIT_ENABLED", "true")
enabled = _parse_bool(enabled_str)

auth_limit = os.getenv("RATE_LIMIT_AUTH", "10/minute")
standard_limit = os.getenv("RATE_LIMIT_STANDARD", "100/minute")
ai_limit = os.getenv("RATE_LIMIT_AI", "20/minute")
websocket_limit = os.getenv("RATE_LIMIT_WEBSOCKET", "30/minute")
storage = os.getenv("RATE_LIMIT_STORAGE", "memory")
redis_url = os.getenv("REDIS_URL")

# Validate storage type
if storage not in ("memory", "redis"):
logger.warning(
f"Invalid RATE_LIMIT_STORAGE: {storage}. "
f"Must be 'memory' or 'redis'. Defaulting to 'memory'."
)
storage = "memory"

# Warn if redis storage is requested but no URL provided
if storage == "redis" and not redis_url:
logger.warning(
"RATE_LIMIT_STORAGE is 'redis' but REDIS_URL is not set. "
"Falling back to in-memory storage."
)
storage = "memory"

return cls(
auth_limit=auth_limit,
standard_limit=standard_limit,
ai_limit=ai_limit,
websocket_limit=websocket_limit,
enabled=enabled,
storage=storage,
redis_url=redis_url,
)


# Global rate limit config instance
_rate_limit_config: Optional[RateLimitConfig] = None


def get_rate_limit_config() -> RateLimitConfig:
"""Get the global rate limit configuration.

Loads from environment on first call, cached thereafter.

Returns:
RateLimitConfig instance
"""
global _rate_limit_config
if _rate_limit_config is None:
_rate_limit_config = RateLimitConfig.from_environment()
logger.info(
f"Rate limit config initialized: "
f"enabled={_rate_limit_config.enabled}, "
f"storage={_rate_limit_config.storage}, "
f"standard={_rate_limit_config.standard_limit}"
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
return _rate_limit_config


def _reset_rate_limit_config() -> None:
"""Reset the global rate limit configuration.

Useful for testing to ensure clean state between tests.
"""
global _rate_limit_config
_rate_limit_config = None
14 changes: 14 additions & 0 deletions codeframe/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,11 @@ class GlobalConfig(BaseSettings):
github_token: Optional[str] = Field(None, alias="GITHUB_TOKEN")
github_repo: Optional[str] = Field(None, alias="GITHUB_REPO") # Format: "owner/repo"

# Rate Limiting Configuration
rate_limit_enabled: bool = Field(True, alias="RATE_LIMIT_ENABLED")
rate_limit_storage: str = Field("memory", alias="RATE_LIMIT_STORAGE")
redis_url: Optional[str] = Field(None, alias="REDIS_URL")

model_config = SettingsConfigDict(
env_file=".env", env_file_encoding="utf-8", case_sensitive=False, extra="ignore"
)
Expand All @@ -417,6 +422,15 @@ def validate_port(cls, v: int) -> int:
raise ValueError(f"API_PORT must be between 1 and 65535, got: {v}")
return v

@field_validator("rate_limit_storage")
@classmethod
def validate_rate_limit_storage(cls, v: str) -> str:
"""Validate rate limit storage is valid."""
allowed = ["memory", "redis"]
if v not in allowed:
raise ValueError(f"RATE_LIMIT_STORAGE must be one of {allowed}, got: {v}")
return v

def get_cors_origins_list(self) -> list[str]:
"""Parse CORS origins from comma-separated string."""
return [origin.strip() for origin in self.cors_origins.split(",") if origin.strip()]
Expand Down
36 changes: 36 additions & 0 deletions codeframe/lib/audit_logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ class AuditEventType(Enum):
USER_DELETED = "user.deleted"
USER_ROLE_CHANGED = "user.role.changed"

# Rate limiting events
RATE_LIMIT_EXCEEDED = "rate_limit.exceeded"
RATE_LIMIT_WARNING = "rate_limit.warning"


class AuditLogger:
"""Centralized audit logger for security events.
Expand Down Expand Up @@ -182,6 +186,38 @@ def log_user_event(
metadata=metadata,
)

def log_rate_limit_event(
self,
event_type: AuditEventType,
user_id: Optional[int] = None,
ip_address: Optional[str] = None,
endpoint: Optional[str] = None,
limit_category: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
) -> None:
"""Log rate limiting event.

Args:
event_type: Type of rate limit event (exceeded or warning)
user_id: User ID (if authenticated)
ip_address: Client IP address
endpoint: API endpoint path
limit_category: Rate limit category (auth, standard, ai, websocket)
metadata: Additional event metadata
"""
self._log_event(
event_type=event_type,
user_id=user_id,
resource_type="rate_limit",
resource_id=None,
ip_address=ip_address,
metadata={
**(metadata or {}),
"endpoint": endpoint,
"limit_category": limit_category,
},
)

def _log_event(
self,
event_type: AuditEventType,
Expand Down
Loading
Loading