E2 (B21): аудит + админ-панель пользователей + гео-скоуп
- Модели audit_events (immutable) + audit_changes (field-level old→new);
Alembic 008_e2_audit.
- backend/audit.py: audit_log() (событие + изменения), audit_retention_days
(90 дней из security_settings), visible_unit_ids (рекурсивный CTE:
своё подразделение + подчинённые; РЦУ РЧС/legacy → None = без фильтра),
can_access_unit, _uuid_hex.
- backend/routers/admin_users.py (manage_users/view_audit):
GET /admin/users (фильтры status/unit/q, скоуп по подразделению),
POST (создание с must_change_password, роль, скоуп-проверка),
PATCH /{id} (роль/юнит/статус/сброс пароля, field-level аудит),
GET /units (справочник подразделений), GET /audit (ретеншн-фильтр).
coordinator управляет только своим юнитом+подчинёнными и не трогает РЦУ.
- Грабли SQLite-тестов: UUID колонки хранят hex без дефисов — сравнения
только в hex-пространстве (_uuid_hex); StaticPool обязателен.
- Тесты E2 (8): создание/блокировка/сброс пароля/скоуп координатора/
cross-oblast 403/без права 403/аудит user_create.
210 passed, 5 skipped.
This commit is contained in:
@@ -334,3 +334,35 @@ class SecuritySetting(Base):
|
||||
|
||||
key = Column(String(50), primary_key=True)
|
||||
value = Column(String(255), nullable=False)
|
||||
|
||||
# ==================== B21/E2: аудит действий ====================
|
||||
|
||||
class AuditEvent(Base):
|
||||
"""Аудит действий (immutable): кто, когда, что. CREATE+READ only."""
|
||||
__tablename__ = "audit_events"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"))
|
||||
username = Column(String(100))
|
||||
event_type = Column(String(40), nullable=False) # user_create|user_update|user_lock|case_create|...
|
||||
object_type = Column(String(40)) # user|case|operation|...
|
||||
object_id = Column(String(64))
|
||||
ip_address = Column(String(64))
|
||||
user_agent = Column(String(255))
|
||||
details = Column(JSONB)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
__table_args__ = (
|
||||
sa.Index("idx_audit_events_created", "created_at"),
|
||||
)
|
||||
|
||||
|
||||
class AuditChange(Base):
|
||||
"""Field-level изменения: old → new (привязаны к AuditEvent)."""
|
||||
__tablename__ = "audit_changes"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
event_id = Column(UUID(as_uuid=True), ForeignKey("audit_events.id", ondelete="CASCADE"), nullable=False)
|
||||
field_name = Column(String(100), nullable=False)
|
||||
old_value = Column(Text)
|
||||
new_value = Column(Text)
|
||||
|
||||
Reference in New Issue
Block a user