"""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 }