Пользователь сам включает TOTP (QR, приложение-аутентификатор); 10 одноразовых кодов восстановления; superadmin сбрасывает 2FA другому пользователю. Вход в два шага: /auth/login → mfa_token, /auth/login/2fa — под теми же лимитами и advisory-lock, что пароль. Секрет шифруется Fernet-ключом TOTP_ENC_KEY, защита от повторного использования кода (totp_last_step), лимиты перебора кода при отключении. Миграция 0015, зависимости pyotp, segno, cryptography. UI: второй шаг входа, диалоги 2FA в меню логина, бейдж и сброс в «Пользователях»; submitDialog не закрывает окно, открытое следующим шагом цепочки. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
34 lines
2.2 KiB
Python
34 lines
2.2 KiB
Python
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
|
|
|
|
database_url: str = "postgresql+psycopg://ipam:ipam@localhost:55432/ipam"
|
|
jwt_secret: str = "" # обязателен: проверяется при старте приложения (validate_secrets), миграциям не нужен
|
|
jwt_ttl_minutes: int = 480
|
|
admin_username: str = "admin"
|
|
admin_password: str = ""
|
|
trusted_proxies: str = "" # CIDR через запятую: от них принимается X-Forwarded-For
|
|
totp_enc_key: str = "" # изменение 037: ключ Fernet для шифрования секретов TOTP; пусто — 2FA недоступна (503), потеря ключа = сброс 2FA всем
|
|
|
|
|
|
settings = Settings()
|
|
|
|
_WEAK_SECRETS = {"change-me", "changeme", "dev-only-secret", "secret", "password"}
|
|
|
|
|
|
def validate_secrets(s: Settings = settings) -> None:
|
|
"""Fail-fast при старте: подпись токенов известным/коротким ключом позволяет подделать токен любого пользователя."""
|
|
if len(s.jwt_secret) < 32 or s.jwt_secret.lower() in _WEAK_SECRETS:
|
|
raise RuntimeError("JWT_SECRET не задан, короче 32 символов или является заглушкой: сгенерируйте значение (python scripts/gen_env.py)")
|
|
if s.admin_password and len(s.admin_password) < 8:
|
|
raise RuntimeError("ADMIN_PASSWORD короче 8 символов: пароль слабее, чем допускает UI")
|
|
if s.totp_enc_key: # изменение 037: не задан — приложение стартует, 2FA недоступна; задан некорректно — отказ старта
|
|
from cryptography.fernet import Fernet
|
|
|
|
try:
|
|
Fernet(s.totp_enc_key.encode())
|
|
except (ValueError, TypeError) as e:
|
|
raise RuntimeError("TOTP_ENC_KEY некорректен: нужен ключ Fernet (python -c \"from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())\")") from e
|