Пентест (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>
65 lines
2.7 KiB
Python
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")
|