2222b7ad5b
- 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
96 lines
4.0 KiB
Python
96 lines
4.0 KiB
Python
"""B23: ретенция журналов — реальное удаление, а не фильтр при чтении.
|
|
|
|
До сих пор «ретенция 90 дней» из security_settings реализовывалась как
|
|
фильтр в GET /admin/users/audit — записи копились в БД вечно. Этот модуль
|
|
удаляет просроченное по-настоящему (DELETE), охватывая:
|
|
|
|
audit_events (+ audit_changes каскадно) — по audit_retention_days
|
|
auth_events — по audit_retention_days
|
|
|
|
Запуск: POST /api/v1/admin/maintenance/purge-logs (право manage_security)
|
|
или вручную:
|
|
docker exec vector-backend python -m backend.purge_logs
|
|
(режим разового запуска — для cron внутри контейнера)
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
from sqlalchemy import text
|
|
from sqlalchemy.orm import Session
|
|
|
|
from backend.logging_setup import get_logger
|
|
from backend.models import AuthEvent, SecuritySetting
|
|
|
|
log = get_logger('retention')
|
|
|
|
|
|
def _retention_days(db: Session, default: int = 90) -> int:
|
|
row = db.query(SecuritySetting).filter(
|
|
SecuritySetting.key == 'audit_retention_days').first()
|
|
try:
|
|
return int(row.value) if row else default
|
|
except (TypeError, ValueError):
|
|
return default
|
|
|
|
|
|
def purge_logs(db: Session, retention_days: int | None = None) -> dict[str, int]:
|
|
"""Удалить записи журналов старше retention_days. Возвращает счётчики.
|
|
|
|
audit_changes удаляются каскадом по FK (ondelete=CASCADE); для SQLite-тестов
|
|
(без каскада на уровне БД) удаляем явно.
|
|
"""
|
|
if retention_days is None:
|
|
retention_days = _retention_days(db)
|
|
cutoff = datetime.now(timezone.utc) - timedelta(days=max(0, retention_days))
|
|
# SQLite отдаёт naive datetimes (DateTime(timezone=True) не respected) —
|
|
# сравнение aware-cutoff с naive-колонкой падает TypeError. Убираем tzinfo:
|
|
cutoff = cutoff.replace(tzinfo=None)
|
|
|
|
audit_deleted = 0
|
|
try:
|
|
from backend.models import AuditChange, AuditEvent
|
|
change_ids = (db.query(AuditChange.id)
|
|
.join(AuditEvent, AuditChange.event_id == AuditEvent.id)
|
|
.filter(AuditEvent.created_at < cutoff).all())
|
|
change_ids = [row[0] for row in change_ids]
|
|
if change_ids:
|
|
(db.query(AuditChange)
|
|
.filter(AuditChange.id.in_(change_ids))
|
|
.delete(synchronize_session=False)) # changes считаются вместе с событием
|
|
audit_deleted += (db.query(AuditEvent)
|
|
.filter(AuditEvent.created_at < cutoff)
|
|
.delete(synchronize_session=False))
|
|
except Exception as e:
|
|
db.rollback()
|
|
log.error('purge audit_events не удался: %s', e)
|
|
raise
|
|
|
|
auth_deleted = 0
|
|
try:
|
|
auth_deleted += (db.query(AuthEvent)
|
|
.filter(AuthEvent.created_at < cutoff)
|
|
.delete(synchronize_session=False))
|
|
except Exception as e:
|
|
log.error('purge auth_events не удался: %s', e)
|
|
raise
|
|
|
|
db.commit()
|
|
if audit_deleted or auth_deleted:
|
|
log.info('retention purge: %d аудит-записей, %d auth-записей удалено (cutoff=%s, %d дн.)',
|
|
audit_deleted, auth_deleted, cutoff.date(), retention_days)
|
|
return {'audit_events': audit_deleted, 'auth_events': auth_deleted}
|
|
|
|
|
|
def _cli() -> None:
|
|
"""Разовый прогон: python -m backend.purge_logs (для cron)."""
|
|
from backend.database import SessionLocal
|
|
with SessionLocal() as db:
|
|
result = purge_logs(db)
|
|
print(f'purge: {result}')
|
|
raise SystemExit(0)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
_cli() |