Files
vector/backend/audit.py
T
Sadmin d5a270f3af B22: прикладное логирование vector.* + администрирование в доках (по образцу РВС)
- backend/logging_setup.py: setup_logging() (stdout INFO + файл /tmp/vector/vector.log,
  ротация 5×2 МБ, VectorLOGLEVEL/VectorLOGFILE/VectorLOGDIR), get_logger('vector.*')
- события: vector.analyze (матрасчёт с длительностью), vector.auth (login_ok/failed/
  lockout), vector.audit (каждая запись), vector.contour (ok/failed/rejected),
  vector.operations (operation_complete), vector.geocode (успех/timeout/ошибка)
- существующие логгеры переведены в namespace vector.* (geo, geo.osm, rules, water, admin)
- README §13а: таблица наблюдаемых событий + правила (без ПДн/секретов)
- SETUP_GUIDE: §15 RPO/RTO + backup≠restore, §16 grep по vector.*, §17 диагностика
  по цепочкам (БД, 500, fallback-зоны, вход), §17а чек-лист передачи проекта
- DEPLOY.md: вместо устаревшего дубля — ссылка на актуальные доки + шпаргалка стенда
- тесты: 239 passed
2026-09-25 09:38:39 +03:00

105 lines
3.6 KiB
Python

"""B21/E2: аудит-хелперы и скоуп подразделений.
audit_log() — запись действия в audit_events (+audit_changes для field-level).
visible_unit_ids() — гео-скоуп: [unit_id] своё подразделение + потомки;
для РЦУ РЧС-admin — None (без фильтра).
"""
from __future__ import annotations
import uuid
from typing import Optional
from sqlalchemy import text
from sqlalchemy.orm import Session
from backend.logging_setup import get_logger
from backend.models import AuditChange, AuditEvent, SecuritySetting, User
log = get_logger('audit')
def _client_meta(request) -> tuple[str | None, str | None]:
if request is None:
return None, None
ip = request.client.host if request.client else None
return ip, (request.headers.get('user-agent') or '')[:255]
def audit_log(
db: Session,
actor: User | None,
event_type: str,
object_type: str | None = None,
object_id: str | None = None,
changes: dict[str, tuple] | None = None,
request=None,
details: dict | None = None,
) -> None:
"""Записать действие в аудит. changes = {field: (old, new)}."""
ip, ua = _client_meta(request)
event = AuditEvent(
user_id=getattr(actor, 'id', None),
username=getattr(actor, 'username', None),
event_type=event_type,
object_type=object_type,
object_id=str(object_id) if object_id else None,
ip_address=ip,
user_agent=ua,
details=details or {},
)
db.add(event)
db.flush()
for field, (old, new) in (changes or {}).items():
db.add(AuditChange(
event_id=event.id,
field_name=field,
old_value=None if old is None else str(old),
new_value=None if new is None else str(new),
))
db.commit()
log.info('audit %s user=%s obj=%s/%s ip=%s', event_type,
event.username or '—', object_type or '—', event.object_id or '—',
event.ip_address or '—')
def audit_retention_days(db: Session) -> int:
row = db.query(SecuritySetting).filter(SecuritySetting.key == 'audit_retention_days').first()
return int(row.value) if row else 90
def _uuid_hex(value) -> str:
"""UUID → hex-строка без дефисов (UUID-колонка хранит hex в sqlite-тестах)."""
if hasattr(value, 'hex'):
return value.hex
return uuid.UUID(str(value)).hex
def visible_unit_ids(db: Session, user: User) -> list[str] | None:
"""Гео-скоуп: [unit_id] свои + подчинённые подразделения.
None = без фильтра (РЦУ РЧС/admin)."""
import uuid
unit_id = getattr(user, 'unit_id', None)
if not unit_id:
return None # РЦУ РЧС или legacy-пользователь без юнита — видит всё
rows = db.execute(
text("""
WITH RECURSIVE tree AS (
SELECT id FROM mchs_units WHERE id = :uid
UNION ALL
SELECT u.id FROM mchs_units u JOIN tree t ON u.parent_id = t.id
)
SELECT id FROM tree
"""),
{'uid': _uuid_hex(unit_id)},
).fetchall()
return [str(r[0]) for r in rows]
def can_access_unit(db: Session, user: User, target_unit_id: str | None) -> bool:
"""Может ли пользователь действовать над объектом принадлежащим target_unit_id."""
scope = visible_unit_ids(db, user)
if scope is None:
return True
if not target_unit_id:
return False
return _uuid_hex(target_unit_id) in scope