Files
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

77 lines
4.1 KiB
Python

from fastapi import APIRouter, Depends, Query
from pydantic import BaseModel
from sqlalchemy import and_, func, select
from sqlalchemy.orm import Session
from app import schemas as s
from app.api.v1.prefixes import _prefix_outs
from app.db import get_db
from app.models import Address, AddressStatus, AuditLog, Prefix, PrefixStatus, User, Vrf
from app.security import current_user
from app.services import capacity, scope_org, utilization # изменение 032
router = APIRouter(dependencies=[Depends(current_user)])
class Overview(BaseModel):
prefixes: int
vrfs: int
assigned: int
capacity: int
utilization: int
reserved: int
top_prefixes: list[s.PrefixOut]
recent_changes: list[s.AuditOut]
def _ipv4_roots(db: Session):
"""Активные IPv4-префиксы, не вложенные ни в один другой активный IPv4-префикс того же VRF (по CIDR, не по
parent_id — дерево может включать неактивные ветки). Основа ёмкости и загрузки «Обзора» (изменение 025, находка №1):
раньше ёмкость считалась суммой листьев, а назначенные адреса — по всем IPv4-адресам, включая нелистовые префиксы."""
q = Prefix.__table__.alias("q")
contained = (
select(1).select_from(q)
.where(q.c.vrf_id == Prefix.vrf_id, q.c.id != Prefix.id, q.c.status == PrefixStatus.active,
func.family(q.c.prefix) == 4, q.c.prefix.op(">>")(Prefix.prefix))
.exists()
)
return select(Prefix.vrf_id, Prefix.prefix).where(Prefix.status == PrefixStatus.active, func.family(Prefix.prefix) == 4, ~contained)
@router.get("/overview", response_model=Overview, tags=["overview"])
def overview(db: Session = Depends(get_db), user: User = Depends(current_user)): # изменение 032
stmt_prefix = select(Prefix).where(Prefix.status == PrefixStatus.active)
stmt_prefix = scope_org(stmt_prefix, Prefix.organization_id, user) # изменение 032
active = db.scalars(stmt_prefix).all()
outs = _prefix_outs(db, list(active))
parents = {p.parent_id for p in outs if p.parent_id}
leaves4 = [p for p in outs if p.id not in parents and p.family == 4] # top_prefixes — как раньше, по листьям
top = sorted((p for p in leaves4 if p.capacity > 1), key=lambda p: p.utilization, reverse=True)[:4]
roots_stmt = _ipv4_roots(db)
roots_stmt = scope_org(roots_stmt, Prefix.organization_id, user) # изменение 032
roots = db.execute(roots_stmt).all()
cap = sum(capacity(str(r.prefix)) for r in roots)
roots_cte = roots_stmt.cte("overview_roots")
counts = dict(db.execute(
select(Address.status, func.count(func.distinct(Address.id)))
.select_from(Address.__table__.join(roots_cte, and_(roots_cte.c.vrf_id == Address.vrf_id, Address.address.op("<<=")(roots_cte.c.prefix))))
.where(func.family(Address.address) == 4, Address.status.in_((AddressStatus.assigned, AddressStatus.reserved)))
.group_by(Address.status)
).all())
assigned = counts.get(AddressStatus.assigned, 0)
reserved = counts.get(AddressStatus.reserved, 0)
stmt_audit = select(AuditLog)
stmt_audit = scope_org(stmt_audit, AuditLog.organization_id, user) # изменение 032: фильтр по организации
recent = db.scalars(stmt_audit.order_by(AuditLog.id.desc()).limit(4)).all()
stmt_prefix_count = select(func.count()).select_from(Prefix)
stmt_prefix_count = scope_org(stmt_prefix_count, Prefix.organization_id, user) # изменение 032
stmt_vrf_count = select(func.count()).select_from(Vrf)
stmt_vrf_count = scope_org(stmt_vrf_count, Vrf.organization_id, user) # изменение 032
return Overview(
prefixes=db.scalar(stmt_prefix_count) or 0,
vrfs=db.scalar(stmt_vrf_count) or 0,
assigned=assigned, capacity=cap, utilization=utilization(assigned, cap), reserved=reserved,
top_prefixes=top, recent_changes=[s.AuditOut.from_row(r) for r in recent],
)