Files
ipam_control/alembic/versions/0011_users_organization_scope.py
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

47 lines
2.3 KiB
Python

"""Добавление привязки пользователей к организациям: admin/viewer привязаны, superadmin нет (изменение 032)
Revision ID: 0011
Revises: 0010
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
revision = "0011"
down_revision = "0010"
branch_labels = None
depends_on = None
def upgrade() -> None:
# Добавляем колонку organization_id
op.add_column("users", sa.Column("organization_id", sa.Integer(), nullable=True))
op.create_index(op.f("ix_users_organization_id"), "users", ["organization_id"], unique=False)
op.create_foreign_key(op.f("fk_users_organization_id_organizations"), "users", "organizations", ["organization_id"], ["id"])
# Backfill: согласно плану (изменение 032, решение 4)
# 1. Пользователи с ролью 'admin' (текущие администраторы) → superadmin, organization_id=NULL
# 2. Пользователи с ролью 'viewer' → organization_id=NULL, is_active=false (заблокированы)
op.execute(sa.text("UPDATE users SET role = 'superadmin', organization_id = NULL WHERE role = 'admin'"))
op.execute(sa.text("UPDATE users SET organization_id = NULL, is_active = false WHERE role = 'viewer'"))
# Добавляем CHECK constraint ПОСЛЕ backfill (иначе может не пройти)
# Инвариант: is_active=false OR (role='superadmin')=(organization_id IS NULL)
# Ослабленный для неактивных — допускает временное состояние viewer без организации
op.create_check_constraint(
"ck_users_role_org_scope",
"users",
"is_active = false OR (role = 'superadmin') = (organization_id IS NULL)",
)
def downgrade() -> None:
op.drop_constraint("ck_users_role_org_scope", "users", type_="check")
op.drop_constraint(op.f("fk_users_organization_id_organizations"), "users", type_="foreignkey")
op.drop_index(op.f("ix_users_organization_id"), table_name="users")
op.drop_column("users", "organization_id")
# Backfill downgrade: вернуть superadmin → admin
op.execute(sa.text("UPDATE users SET role = 'admin' WHERE role = 'superadmin'"))