B23: политика паролей исполняется + ретенция журналов по-настоящему

- backend/password_policy.py: validate_new_password (длина, сложность
  буквы+цифры, сверка с текущим и историей), record_password_change
  (хранит ПРЕДЫДУШИЙ хэш, обрезка до password_history_count),
  password_expired + enforce_expiry (пометка must_change_password)
- users.password_changed_at + таблица password_history (010_b23_policy)
- change-password и админ-сброс теперь под полной политикой (сложность
  и переиспользование вместо голой длины); get_current_user проверяет
  срок действия каждого пароля при запросе
- backend/purge_logs.py: реальный DELETE просроченных audit_events
  (+changes) и auth_events; POST /admin/users/maintenance/purge-logs
  (manage_security) + CLI python -m backend.purge_logs для cron
- тесты: +19 (258 passed) — валидация/история/срок, purge/идемпотентность/403
This commit is contained in:
2026-09-25 09:56:26 +03:00
parent d5a270f3af
commit 2222b7ad5b
7 changed files with 585 additions and 11 deletions
+17
View File
@@ -33,6 +33,8 @@ class User(Base):
must_change_password = Column(Boolean, default=False)
created_by = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"))
updated_at = Column(DateTime, server_default=func.now())
# B23: политика паролей (expiry/history исполняется в routers/auth.py)
password_changed_at = Column(DateTime(timezone=True))
class Case(Base):
@@ -335,6 +337,21 @@ class SecuritySetting(Base):
key = Column(String(50), primary_key=True)
value = Column(String(255), nullable=False)
class PasswordHistoryEntry(Base):
"""B23: история паролей (bcrypt-хэши). Записывается при каждой смене,
проверяется против password_history_count последних записей."""
__tablename__ = "password_history"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
hashed_password = Column(String(255), nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
__table_args__ = (
sa.Index("idx_password_history_user", "user_id", "created_at"),
)
# ==================== B21/E2: аудит действий ====================
class SearchOperation(Base):