Files
app-service/backend/app/core/password_policy.py
matteoscrugli 8c4a555b88 Add comprehensive backend features and mobile UI improvements
Backend:
- Add 2FA authentication with TOTP support
- Add API keys management system
- Add audit logging for security events
- Add file upload/management system
- Add notifications system with preferences
- Add session management
- Add webhooks integration
- Add analytics endpoints
- Add export functionality
- Add password policy enforcement
- Add new database migrations for core tables

Frontend:
- Add module position system (top/bottom sidebar sections)
- Add search and notifications module configuration tabs
- Add mobile logo replacing hamburger menu
- Center page title absolutely when no tabs present
- Align sidebar footer toggles with navigation items
- Add lighter icon color in dark theme for mobile
- Add API keys management page
- Add notifications page with context
- Add admin analytics and audit logs pages

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 22:27:32 +01:00

241 lines
7.6 KiB
Python

"""Password policy validation and enforcement."""
import re
from typing import List, Optional
from dataclasses import dataclass
from sqlalchemy.orm import Session
from app import crud
@dataclass
class PasswordPolicy:
"""Password policy configuration."""
min_length: int = 8
max_length: int = 128
require_uppercase: bool = True
require_lowercase: bool = True
require_digit: bool = True
require_special: bool = False
special_characters: str = "!@#$%^&*()_+-=[]{}|;:,.<>?"
disallow_username: bool = True
disallow_email: bool = True
disallow_common: bool = True
# Common passwords to disallow
COMMON_PASSWORDS = {
"password", "123456", "12345678", "qwerty", "abc123",
"monkey", "1234567", "letmein", "trustno1", "dragon",
"baseball", "iloveyou", "master", "sunshine", "ashley",
"bailey", "passw0rd", "shadow", "123123", "654321",
"superman", "qazwsx", "michael", "football", "password1",
"password123", "welcome", "jesus", "ninja", "mustang",
"admin", "admin123", "root", "toor", "administrator"
}
def get_password_policy(db: Optional[Session] = None) -> PasswordPolicy:
"""Get password policy from settings or use defaults."""
policy = PasswordPolicy()
if db:
try:
# Load settings from database
min_length = crud.settings.get_setting_value(db, "password_min_length")
if min_length is not None:
policy.min_length = int(min_length)
require_uppercase = crud.settings.get_setting_value(db, "password_require_uppercase")
if require_uppercase is not None:
policy.require_uppercase = str(require_uppercase).lower() in ("true", "1")
require_lowercase = crud.settings.get_setting_value(db, "password_require_lowercase")
if require_lowercase is not None:
policy.require_lowercase = str(require_lowercase).lower() in ("true", "1")
require_digit = crud.settings.get_setting_value(db, "password_require_digit")
if require_digit is not None:
policy.require_digit = str(require_digit).lower() in ("true", "1")
require_special = crud.settings.get_setting_value(db, "password_require_special")
if require_special is not None:
policy.require_special = str(require_special).lower() in ("true", "1")
except Exception:
pass # Use defaults on error
return policy
def validate_password(
password: str,
policy: Optional[PasswordPolicy] = None,
username: Optional[str] = None,
email: Optional[str] = None
) -> List[str]:
"""
Validate password against policy.
Returns list of validation errors (empty list if valid).
"""
if policy is None:
policy = PasswordPolicy()
errors = []
# Length checks
if len(password) < policy.min_length:
errors.append(f"Password must be at least {policy.min_length} characters long")
if len(password) > policy.max_length:
errors.append(f"Password must not exceed {policy.max_length} characters")
# Character requirements
if policy.require_uppercase and not re.search(r"[A-Z]", password):
errors.append("Password must contain at least one uppercase letter")
if policy.require_lowercase and not re.search(r"[a-z]", password):
errors.append("Password must contain at least one lowercase letter")
if policy.require_digit and not re.search(r"\d", password):
errors.append("Password must contain at least one digit")
if policy.require_special:
special_pattern = f"[{re.escape(policy.special_characters)}]"
if not re.search(special_pattern, password):
errors.append("Password must contain at least one special character")
# Disallow username/email in password
password_lower = password.lower()
if policy.disallow_username and username:
if username.lower() in password_lower:
errors.append("Password must not contain your username")
if policy.disallow_email and email:
email_parts = email.lower().split("@")
if email_parts[0] in password_lower:
errors.append("Password must not contain your email address")
# Disallow common passwords
if policy.disallow_common:
if password_lower in COMMON_PASSWORDS:
errors.append("Password is too common. Please choose a stronger password")
return errors
def get_password_requirements(policy: Optional[PasswordPolicy] = None) -> dict:
"""Get password requirements as a dictionary (for frontend display)."""
if policy is None:
policy = PasswordPolicy()
requirements = {
"min_length": policy.min_length,
"max_length": policy.max_length,
"require_uppercase": policy.require_uppercase,
"require_lowercase": policy.require_lowercase,
"require_digit": policy.require_digit,
"require_special": policy.require_special,
}
# Generate human-readable description
rules = [f"At least {policy.min_length} characters"]
if policy.require_uppercase:
rules.append("At least one uppercase letter (A-Z)")
if policy.require_lowercase:
rules.append("At least one lowercase letter (a-z)")
if policy.require_digit:
rules.append("At least one digit (0-9)")
if policy.require_special:
rules.append(f"At least one special character ({policy.special_characters[:10]}...)")
requirements["rules"] = rules
return requirements
def check_password_strength(password: str) -> dict:
"""
Check password strength and return a score.
Returns dict with score (0-100), level (weak/medium/strong/very_strong), and feedback.
"""
score = 0
feedback = []
# Length scoring
length = len(password)
if length >= 8:
score += 20
if length >= 12:
score += 10
if length >= 16:
score += 10
# Character variety scoring
has_upper = bool(re.search(r"[A-Z]", password))
has_lower = bool(re.search(r"[a-z]", password))
has_digit = bool(re.search(r"\d", password))
has_special = bool(re.search(r"[!@#$%^&*()_+\-=\[\]{}|;:,.<>?]", password))
if has_upper:
score += 15
if has_lower:
score += 15
if has_digit:
score += 15
if has_special:
score += 15
# Bonus for mixing character types
variety = sum([has_upper, has_lower, has_digit, has_special])
if variety >= 3:
score += 10
if variety == 4:
score += 10
# Penalties
if password.lower() in COMMON_PASSWORDS:
score = min(score, 20)
feedback.append("This is a commonly used password")
if re.search(r"(.)\1{2,}", password):
score -= 10
feedback.append("Avoid repeated characters")
if re.search(r"(012|123|234|345|456|567|678|789|890|abc|bcd|cde|def)", password.lower()):
score -= 10
feedback.append("Avoid sequential characters")
# Ensure score is in valid range
score = max(0, min(100, score))
# Determine strength level
if score >= 80:
level = "very_strong"
elif score >= 60:
level = "strong"
elif score >= 40:
level = "medium"
else:
level = "weak"
# Add suggestions
if not has_upper:
feedback.append("Add uppercase letters")
if not has_lower:
feedback.append("Add lowercase letters")
if not has_digit:
feedback.append("Add numbers")
if not has_special:
feedback.append("Add special characters")
if length < 12:
feedback.append("Use a longer password")
return {
"score": score,
"level": level,
"feedback": feedback[:3] # Limit to top 3 suggestions
}