mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-19 04:40:32 -05:00
e34a668ddc
Introduce AUTH_METHOD values ldap and all, with LDAP_* environment settings, ldap3-based LDAPService (search, optional groupOfNames checks, user bind, DB sync), and users.auth_provider (local|oidc|ldap) via migration 153_add_user_auth_provider. Login supports LDAP-only and combined all (local then LDAP where appropriate); OIDC callback sets auth_provider. Forgot/reset/change password flows skip LDAP-managed accounts. Admin System Settings gains a read-only LDAP summary and POST /admin/ldap/test. Production env validation requires core LDAP variables when LDAP is enabled; OIDC registration and docs recognize all. Documentation: new docs/admin/configuration/LDAP_SETUP.md; updates to OIDC_SETUP, GETTING_STARTED, Docker guides, Render deploy notes, docs README, and CHANGELOG. Tests: tests/test_ldap_auth.py; test_oidc_logout allows auth_method all.
199 lines
6.0 KiB
Python
199 lines
6.0 KiB
Python
"""
|
|
Environment variable validation on startup.
|
|
Ensures required configuration is present and valid.
|
|
"""
|
|
|
|
import os
|
|
from typing import Dict, List, Optional, Tuple
|
|
|
|
from flask import current_app
|
|
|
|
|
|
class EnvValidationError(Exception):
|
|
"""Raised when environment validation fails"""
|
|
|
|
pass
|
|
|
|
|
|
def validate_required_env_vars(required_vars: List[str], raise_on_error: bool = True) -> Tuple[bool, List[str]]:
|
|
"""
|
|
Validate that required environment variables are set.
|
|
|
|
Args:
|
|
required_vars: List of required environment variable names
|
|
raise_on_error: If True, raise EnvValidationError on failure
|
|
|
|
Returns:
|
|
Tuple of (is_valid, missing_vars)
|
|
"""
|
|
missing = []
|
|
for var in required_vars:
|
|
value = os.getenv(var)
|
|
if not value or value.strip() == "":
|
|
missing.append(var)
|
|
|
|
if missing and raise_on_error:
|
|
raise EnvValidationError(f"Missing required environment variables: {', '.join(missing)}")
|
|
|
|
return len(missing) == 0, missing
|
|
|
|
|
|
def validate_secret_key() -> bool:
|
|
"""
|
|
Validate that SECRET_KEY is set and secure.
|
|
|
|
Returns:
|
|
True if valid, False otherwise
|
|
"""
|
|
secret_key = os.getenv("SECRET_KEY", "")
|
|
placeholder_values = {"dev-secret-key-change-in-production", "your-secret-key-change-this", "your-secret-key-here"}
|
|
|
|
if not secret_key:
|
|
return False
|
|
|
|
if secret_key in placeholder_values:
|
|
return False
|
|
|
|
if len(secret_key) < 32:
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def validate_database_url() -> bool:
|
|
"""
|
|
Validate that DATABASE_URL is set and valid.
|
|
|
|
Returns:
|
|
True if valid, False otherwise
|
|
"""
|
|
database_url = os.getenv("DATABASE_URL", "")
|
|
|
|
if not database_url:
|
|
# Check for PostgreSQL env vars
|
|
if all([os.getenv("POSTGRES_DB"), os.getenv("POSTGRES_USER"), os.getenv("POSTGRES_PASSWORD")]):
|
|
return True
|
|
return False
|
|
|
|
# Basic validation - check for known database schemes
|
|
valid_schemes = ["postgresql", "postgresql+psycopg2", "sqlite"]
|
|
if not any(database_url.startswith(scheme) for scheme in valid_schemes):
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def validate_production_config() -> Tuple[bool, List[str]]:
|
|
"""
|
|
Validate production configuration requirements.
|
|
|
|
Returns:
|
|
Tuple of (is_valid, issues)
|
|
"""
|
|
issues = []
|
|
|
|
# Check SECRET_KEY
|
|
if not validate_secret_key():
|
|
issues.append("SECRET_KEY must be set and at least 32 characters long")
|
|
|
|
# Check database
|
|
if not validate_database_url():
|
|
issues.append("DATABASE_URL or PostgreSQL environment variables must be set")
|
|
|
|
# Check HTTPS settings in production
|
|
flask_env = os.getenv("FLASK_ENV", "production")
|
|
if flask_env == "production":
|
|
session_secure = os.getenv("SESSION_COOKIE_SECURE", "false").lower() == "true"
|
|
if not session_secure:
|
|
issues.append("SESSION_COOKIE_SECURE should be true in production")
|
|
|
|
# LDAP required vars when LDAP authentication is enabled
|
|
auth_method = (os.getenv("AUTH_METHOD", "local") or "local").strip().lower()
|
|
if auth_method in ("ldap", "all"):
|
|
for var in ("LDAP_HOST", "LDAP_BASE_DN", "LDAP_BIND_DN", "LDAP_BIND_PASSWORD"):
|
|
val = (os.getenv(var) or "").strip()
|
|
if not val:
|
|
issues.append(f"{var} must be set when AUTH_METHOD enables LDAP ({auth_method})")
|
|
|
|
return len(issues) == 0, issues
|
|
|
|
|
|
def validate_optional_env_vars() -> Dict[str, bool]:
|
|
"""
|
|
Validate optional environment variables and return their status.
|
|
|
|
Returns:
|
|
Dict mapping env var names to their validation status
|
|
"""
|
|
auth_m = (os.getenv("AUTH_METHOD", "") or "").strip().lower()
|
|
oidc_required = auth_m in ("oidc", "both", "all")
|
|
|
|
optional_vars = {
|
|
"TZ": lambda v: bool(v),
|
|
"CURRENCY": lambda v: bool(v),
|
|
"OIDC_ISSUER": lambda v: bool(v) if oidc_required else True,
|
|
"OIDC_CLIENT_ID": lambda v: bool(v) if oidc_required else True,
|
|
"OIDC_CLIENT_SECRET": lambda v: bool(v) if oidc_required else True,
|
|
}
|
|
|
|
results = {}
|
|
for var, validator in optional_vars.items():
|
|
value = os.getenv(var, "")
|
|
results[var] = validator(value)
|
|
|
|
return results
|
|
|
|
|
|
def validate_all(raise_on_error: bool = False) -> Tuple[bool, Dict[str, any]]:
|
|
"""
|
|
Validate all environment configuration.
|
|
|
|
Args:
|
|
raise_on_error: If True, raise EnvValidationError on critical failures
|
|
|
|
Returns:
|
|
Tuple of (is_valid, validation_results)
|
|
"""
|
|
results = {"required": {}, "optional": {}, "production": {}, "warnings": []}
|
|
|
|
# Required vars (minimal set)
|
|
required_vars = [] # Most vars have defaults, but SECRET_KEY is critical in production
|
|
is_production = os.getenv("FLASK_ENV", "production") == "production"
|
|
|
|
if is_production:
|
|
required_vars = ["SECRET_KEY"]
|
|
|
|
is_valid, missing = validate_required_env_vars(required_vars, raise_on_error=False)
|
|
results["required"] = {"valid": is_valid, "missing": missing}
|
|
|
|
# Secret key validation
|
|
secret_valid = validate_secret_key()
|
|
if not secret_valid and is_production:
|
|
results["warnings"].append("SECRET_KEY is not secure for production")
|
|
|
|
# Database validation
|
|
db_valid = validate_database_url()
|
|
results["required"]["database_valid"] = db_valid
|
|
|
|
# Production config validation
|
|
prod_valid, prod_issues = validate_production_config()
|
|
results["production"] = {"valid": prod_valid, "issues": prod_issues}
|
|
|
|
# Optional vars
|
|
results["optional"] = validate_optional_env_vars()
|
|
|
|
# Overall validity
|
|
overall_valid = is_valid and db_valid and (not is_production or prod_valid)
|
|
|
|
if not overall_valid and raise_on_error:
|
|
error_msg = "Environment validation failed:\n"
|
|
if missing:
|
|
error_msg += f" Missing: {', '.join(missing)}\n"
|
|
if not db_valid:
|
|
error_msg += " Database configuration invalid\n"
|
|
if prod_issues:
|
|
error_msg += f" Production issues: {', '.join(prod_issues)}\n"
|
|
raise EnvValidationError(error_msg.strip())
|
|
|
|
return overall_valid, results
|