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>
241 lines
7.6 KiB
Python
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
|
|
}
|