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:
pacnpal
2025-12-23 16:41:42 -05:00
parent ae31e889d7
commit edcd8f2076
155 changed files with 22046 additions and 4645 deletions

View 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")