Files
ipam_control/alembic/versions/0012_audit_log_organization.py
T
ayurishchevandClaude Opus 5.5 744a025960 Задачи 032-033: ролевая модель с привязкой к организации, исправления по ревью
Пентест (docs/reviews/2026-09-27-pentest.md) и план 031 (Swagger, TLS) — план, не реализован.
032 Роль superadmin (без организации) и привязка admin/viewer к одной организации:
    users.organization_id + CHECK, audit_log.organization_id (миграции 0010-0012);
    require_org/scope_org во всех чтениях и записях, журнал и «Обзор» в границах
    организации; пользователи, организации, типы устройств, настройки журнала — только superadmin.
033 Исправление находок ревью 032 (docs/reviews/2026-09-27-changes-032-review.md,
    docs/reviews/2026-09-27-codebase-review.md):
    - FK audit_log.organization_id ON DELETE SET NULL (миграция 0013) — удаление организаций;
    - проверка организации в предпросмотре подсети;
    - инвариант «роль — организация» по итоговому состоянию (повышение снимает организацию,
      понижение требует её), 422/404 вместо обезличенных 409;
    - одинаковый 404 для чужих и несуществующих объектов (VRF, устройство, parent_id, оператор);
    - отказы удаления в журнале организации, счётчики типов в пределах организации;
    - UI: живое поле «Организация» в диалоге пользователя, бейдж superadmin; род в текстах 404.
README актуализирован под ролевую модель.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 11:26:57 +03:00

65 lines
2.7 KiB
Python

"""Добавление привязки записей журнала к организациям для изоляции по организациям (изменение 032)
Revision ID: 0012
Revises: 0011
"""
from alembic import op
import sqlalchemy as sa
revision = "0012"
down_revision = "0011"
branch_labels = None
depends_on = None
def upgrade() -> None:
# Добавляем колонку organization_id в audit_log
op.add_column("audit_log", sa.Column("organization_id", sa.Integer(), nullable=True))
op.create_index(op.f("ix_audit_log_organization_id"), "audit_log", ["organization_id"], unique=False)
op.create_foreign_key(op.f("fk_audit_log_organization_id_organizations"), "audit_log", "organizations", ["organization_id"], ["id"])
# Backfill: привязываем записи к организациям через entity_id
# Для сущностей, у которых есть organization_id (organization, vrf, device, isp, prefix),
# подтягиваем значение из таблицы сущности; для остальных остаётся NULL (видно только superadmin)
# Для сущностей, которые уже удалены, organisation_id остаётся NULL
# (осознанный компромисс — не полная историческая реконструкция)
# organization events
op.execute(sa.text(
"UPDATE audit_log SET organization_id = o.id "
"FROM organizations o WHERE audit_log.entity_type = 'organization' AND audit_log.entity_id = o.id"
))
# vrf events
op.execute(sa.text(
"UPDATE audit_log SET organization_id = v.organization_id "
"FROM vrfs v WHERE audit_log.entity_type = 'vrf' AND audit_log.entity_id = v.id"
))
# device events
op.execute(sa.text(
"UPDATE audit_log SET organization_id = d.organization_id "
"FROM devices d WHERE audit_log.entity_type = 'device' AND audit_log.entity_id = d.id"
))
# isp events
op.execute(sa.text(
"UPDATE audit_log SET organization_id = i.organization_id "
"FROM isps i WHERE audit_log.entity_type = 'isp' AND audit_log.entity_id = i.id"
))
# prefix events (через vrf)
op.execute(sa.text(
"UPDATE audit_log SET organization_id = v.organization_id "
"FROM prefixes p JOIN vrfs v ON p.vrf_id = v.id "
"WHERE audit_log.entity_type = 'prefix' AND audit_log.entity_id = p.id"
))
def downgrade() -> None:
op.drop_constraint(op.f("fk_audit_log_organization_id_organizations"), "audit_log", type_="foreignkey")
op.drop_index(op.f("ix_audit_log_organization_id"), table_name="audit_log")
op.drop_column("audit_log", "organization_id")