mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-24 15:31:09 -05:00
Add secret management guide, client-side performance monitoring, and search accessibility enhancements
- Introduced a comprehensive Secret Management Guide detailing best practices, secret classification, development setup, production management, rotation procedures, and emergency protocols. - Implemented a client-side performance monitoring script to track various metrics including page load performance, paint metrics, layout shifts, and memory usage. - Enhanced search accessibility with keyboard navigation support for search results, ensuring compliance with WCAG standards and improving user experience.
This commit is contained in:
391
backend/config/settings/secrets.py
Normal file
391
backend/config/settings/secrets.py
Normal file
@@ -0,0 +1,391 @@
|
||||
"""
|
||||
Secret management configuration for thrillwiki project.
|
||||
|
||||
This module provides patterns for secure secret handling including:
|
||||
- Secret validation
|
||||
- Secret rotation support
|
||||
- Integration points for secret management services
|
||||
- Secure fallback to environment variables
|
||||
|
||||
For production, consider integrating with:
|
||||
- AWS Secrets Manager
|
||||
- HashiCorp Vault
|
||||
- Google Secret Manager
|
||||
- Azure Key Vault
|
||||
|
||||
Why python-decouple?
|
||||
- Already used across the project for consistency
|
||||
- Provides secure environment variable handling
|
||||
- Supports .env files and environment variables
|
||||
"""
|
||||
|
||||
import logging
|
||||
import warnings
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
from decouple import config, UndefinedValueError
|
||||
|
||||
logger = logging.getLogger("security")
|
||||
|
||||
# =============================================================================
|
||||
# Secret Configuration
|
||||
# =============================================================================
|
||||
|
||||
# Enable secret rotation checking (set to True in production)
|
||||
SECRET_ROTATION_ENABLED = config(
|
||||
"SECRET_ROTATION_ENABLED", default=False, cast=bool
|
||||
)
|
||||
|
||||
# Secret version for tracking rotations
|
||||
SECRET_KEY_VERSION = config("SECRET_KEY_VERSION", default="1")
|
||||
|
||||
# Secret expiry warning threshold (days before expiry to start warning)
|
||||
SECRET_EXPIRY_WARNING_DAYS = config(
|
||||
"SECRET_EXPIRY_WARNING_DAYS", default=30, cast=int
|
||||
)
|
||||
|
||||
# =============================================================================
|
||||
# Required Secrets Registry
|
||||
# =============================================================================
|
||||
# List of required secrets with validation rules
|
||||
|
||||
REQUIRED_SECRETS = {
|
||||
"SECRET_KEY": {
|
||||
"min_length": 50,
|
||||
"description": "Django secret key for cryptographic signing",
|
||||
"rotation_period_days": 90,
|
||||
},
|
||||
"DATABASE_URL": {
|
||||
"min_length": 10,
|
||||
"description": "Database connection URL",
|
||||
"contains_password": True,
|
||||
},
|
||||
}
|
||||
|
||||
# Optional secrets that should be validated if present
|
||||
OPTIONAL_SECRETS = {
|
||||
"SENTRY_DSN": {
|
||||
"min_length": 10,
|
||||
"description": "Sentry error tracking DSN",
|
||||
},
|
||||
"CLOUDFLARE_IMAGES_API_TOKEN": {
|
||||
"min_length": 20,
|
||||
"description": "Cloudflare Images API token",
|
||||
},
|
||||
"FORWARD_EMAIL_API_KEY": {
|
||||
"min_length": 10,
|
||||
"description": "ForwardEmail API key",
|
||||
},
|
||||
"TURNSTILE_SECRET_KEY": {
|
||||
"min_length": 10,
|
||||
"description": "Cloudflare Turnstile secret key",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Secret Validation Functions
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def validate_secret_strength(name: str, value: str, min_length: int = 10) -> bool:
|
||||
"""
|
||||
Validate that a secret meets minimum strength requirements.
|
||||
|
||||
Args:
|
||||
name: Name of the secret (for logging)
|
||||
value: The secret value to validate
|
||||
min_length: Minimum required length
|
||||
|
||||
Returns:
|
||||
bool: True if valid, False otherwise
|
||||
"""
|
||||
if not value:
|
||||
logger.error(f"Secret '{name}' is empty or not set")
|
||||
return False
|
||||
|
||||
if len(value) < min_length:
|
||||
logger.error(
|
||||
f"Secret '{name}' is too short ({len(value)} chars, "
|
||||
f"minimum {min_length})"
|
||||
)
|
||||
return False
|
||||
|
||||
# Check for placeholder values
|
||||
placeholder_patterns = [
|
||||
"your-secret-key",
|
||||
"change-me",
|
||||
"placeholder",
|
||||
"example",
|
||||
"xxx",
|
||||
"todo",
|
||||
]
|
||||
|
||||
value_lower = value.lower()
|
||||
for pattern in placeholder_patterns:
|
||||
if pattern in value_lower:
|
||||
logger.warning(
|
||||
f"Secret '{name}' appears to contain a placeholder value"
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def validate_secret_key(secret_key: str) -> bool:
|
||||
"""
|
||||
Validate Django SECRET_KEY meets security requirements.
|
||||
|
||||
Requirements:
|
||||
- At least 50 characters
|
||||
- Contains mixed case letters
|
||||
- Contains numbers
|
||||
- Contains special characters
|
||||
|
||||
Args:
|
||||
secret_key: The SECRET_KEY value
|
||||
|
||||
Returns:
|
||||
bool: True if valid, False otherwise
|
||||
"""
|
||||
if len(secret_key) < 50:
|
||||
logger.error(
|
||||
f"SECRET_KEY is too short ({len(secret_key)} chars, minimum 50)"
|
||||
)
|
||||
return False
|
||||
|
||||
has_upper = any(c.isupper() for c in secret_key)
|
||||
has_lower = any(c.islower() for c in secret_key)
|
||||
has_digit = any(c.isdigit() for c in secret_key)
|
||||
has_special = any(not c.isalnum() for c in secret_key)
|
||||
|
||||
if not all([has_upper, has_lower, has_digit, has_special]):
|
||||
logger.warning(
|
||||
"SECRET_KEY should contain uppercase, lowercase, digits, "
|
||||
"and special characters"
|
||||
)
|
||||
# Don't fail, just warn - some generated keys may not have all
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def get_secret(
|
||||
name: str,
|
||||
default: Optional[str] = None,
|
||||
required: bool = True,
|
||||
min_length: int = 0,
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Safely retrieve a secret with validation.
|
||||
|
||||
Args:
|
||||
name: Environment variable name
|
||||
default: Default value if not set
|
||||
required: Whether the secret is required
|
||||
min_length: Minimum required length
|
||||
|
||||
Returns:
|
||||
The secret value or None if not found and not required
|
||||
|
||||
Raises:
|
||||
ValueError: If required secret is missing or invalid
|
||||
"""
|
||||
try:
|
||||
value = config(name, default=default)
|
||||
except UndefinedValueError:
|
||||
if required:
|
||||
raise ValueError(f"Required secret '{name}' is not set")
|
||||
return default
|
||||
|
||||
if value and min_length > 0:
|
||||
if not validate_secret_strength(name, value, min_length):
|
||||
if required:
|
||||
raise ValueError(f"Secret '{name}' does not meet requirements")
|
||||
return default
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def validate_required_secrets(raise_on_error: bool = False) -> list[str]:
|
||||
"""
|
||||
Validate all required secrets are set and meet requirements.
|
||||
|
||||
Args:
|
||||
raise_on_error: If True, raise ValueError on first error
|
||||
|
||||
Returns:
|
||||
List of error messages (empty if all valid)
|
||||
"""
|
||||
errors = []
|
||||
|
||||
for name, rules in REQUIRED_SECRETS.items():
|
||||
try:
|
||||
value = config(name)
|
||||
min_length = rules.get("min_length", 0)
|
||||
|
||||
if not validate_secret_strength(name, value, min_length):
|
||||
msg = f"Secret '{name}' validation failed"
|
||||
errors.append(msg)
|
||||
if raise_on_error:
|
||||
raise ValueError(msg)
|
||||
|
||||
except UndefinedValueError:
|
||||
msg = f"Required secret '{name}' is not set: {rules['description']}"
|
||||
errors.append(msg)
|
||||
if raise_on_error:
|
||||
raise ValueError(msg)
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def check_secret_expiry() -> list[str]:
|
||||
"""
|
||||
Check if any secrets are approaching expiry.
|
||||
|
||||
This is a placeholder for integration with secret management services
|
||||
that track secret expiry dates.
|
||||
|
||||
Returns:
|
||||
List of warning messages for secrets approaching expiry
|
||||
"""
|
||||
warnings_list = []
|
||||
|
||||
# Placeholder: In production, integrate with your secret manager
|
||||
# to check actual expiry dates
|
||||
|
||||
# Example check based on version
|
||||
if SECRET_ROTATION_ENABLED:
|
||||
try:
|
||||
version = int(SECRET_KEY_VERSION)
|
||||
# If version is very old, suggest rotation
|
||||
if version < 2:
|
||||
warnings_list.append(
|
||||
"SECRET_KEY version is old. Consider rotating secrets."
|
||||
)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return warnings_list
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Secret Provider Integration Points
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class SecretProvider:
|
||||
"""
|
||||
Base class for secret provider integrations.
|
||||
|
||||
Subclass this to integrate with secret management services:
|
||||
- AWS Secrets Manager
|
||||
- HashiCorp Vault
|
||||
- Google Secret Manager
|
||||
- Azure Key Vault
|
||||
"""
|
||||
|
||||
def get_secret(self, name: str) -> Optional[str]:
|
||||
"""Retrieve a secret by name."""
|
||||
raise NotImplementedError
|
||||
|
||||
def set_secret(self, name: str, value: str) -> bool:
|
||||
"""Set a secret value."""
|
||||
raise NotImplementedError
|
||||
|
||||
def rotate_secret(self, name: str) -> str:
|
||||
"""Rotate a secret and return the new value."""
|
||||
raise NotImplementedError
|
||||
|
||||
def list_secrets(self) -> list[str]:
|
||||
"""List all available secrets."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class EnvironmentSecretProvider(SecretProvider):
|
||||
"""
|
||||
Default secret provider using environment variables.
|
||||
|
||||
This is the fallback provider for development and simple deployments.
|
||||
"""
|
||||
|
||||
def get_secret(self, name: str) -> Optional[str]:
|
||||
"""Retrieve a secret from environment variables."""
|
||||
try:
|
||||
return config(name)
|
||||
except UndefinedValueError:
|
||||
return None
|
||||
|
||||
def set_secret(self, name: str, value: str) -> bool:
|
||||
"""Environment variables are read-only at runtime."""
|
||||
logger.warning(
|
||||
f"Cannot set secret '{name}' in environment provider. "
|
||||
"Update your .env file or environment variables."
|
||||
)
|
||||
return False
|
||||
|
||||
def rotate_secret(self, name: str) -> str:
|
||||
"""Cannot rotate secrets in environment provider."""
|
||||
raise NotImplementedError(
|
||||
"Secret rotation is not supported for environment variables. "
|
||||
"Use a proper secret management service in production."
|
||||
)
|
||||
|
||||
def list_secrets(self) -> list[str]:
|
||||
"""List all known secret names."""
|
||||
return list(REQUIRED_SECRETS.keys()) + list(OPTIONAL_SECRETS.keys())
|
||||
|
||||
|
||||
# Default provider instance
|
||||
_secret_provider: SecretProvider = EnvironmentSecretProvider()
|
||||
|
||||
|
||||
def get_secret_provider() -> SecretProvider:
|
||||
"""Get the current secret provider instance."""
|
||||
return _secret_provider
|
||||
|
||||
|
||||
def set_secret_provider(provider: SecretProvider) -> None:
|
||||
"""Set a custom secret provider."""
|
||||
global _secret_provider
|
||||
_secret_provider = provider
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Startup Validation
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def run_startup_validation() -> None:
|
||||
"""
|
||||
Run secret validation on application startup.
|
||||
|
||||
This function should be called during Django initialization
|
||||
to catch configuration errors early.
|
||||
"""
|
||||
debug_mode = config("DEBUG", default=True, cast=bool)
|
||||
|
||||
# Validate required secrets
|
||||
errors = validate_required_secrets(raise_on_error=not debug_mode)
|
||||
|
||||
if errors:
|
||||
for error in errors:
|
||||
if debug_mode:
|
||||
warnings.warn(f"Secret validation warning: {error}")
|
||||
else:
|
||||
logger.error(f"Secret validation error: {error}")
|
||||
|
||||
# Check for expiring secrets
|
||||
if SECRET_ROTATION_ENABLED:
|
||||
expiry_warnings = check_secret_expiry()
|
||||
for warning in expiry_warnings:
|
||||
logger.warning(f"Secret expiry: {warning}")
|
||||
|
||||
# Validate SECRET_KEY specifically
|
||||
try:
|
||||
secret_key = config("SECRET_KEY")
|
||||
if not validate_secret_key(secret_key):
|
||||
if not debug_mode:
|
||||
raise ValueError("SECRET_KEY does not meet security requirements")
|
||||
except UndefinedValueError:
|
||||
if not debug_mode:
|
||||
raise ValueError("SECRET_KEY is required in production")
|
||||
Reference in New Issue
Block a user