Задачи 011-024: доработки по ревью кодовой базы и исправление находок
Ревью кодовой базы (docs/reviews/2026-09-26-codebase-review.md) и планы по каждой находке:
011 IP уникален в VRF и хранится в самом узком префиксе (addresses.vrf_id, составной FK
с каскадом при переносе VRF, миграция 0007 с остановкой на дублях).
012 Ограничение попыток входа (login_attempts, 429 + Retry-After), выравнивание времени
ответа, журнал без вытеснения анонимными событиями (миграция 0006).
013 Границы пагинации: отрицательные/чрезмерные limit/offset дают 422 вместо 500.
014 Экран адресов: страница свободных адресов арифметикой, пагинация в SQL.
015 Запрет адреса сети/broadcast, загрузка не выше 100 %.
016 Роль по умолчанию — viewer.
017 Проверка JWT_SECRET/ADMIN_PASSWORD при старте.
018 null в PATCH очищает текстовые поля; нейтральный текст конфликта БД.
019 Пакетная загрузка в списках вместо N+1.
020 Автоназначение адреса вне вложенных префиксов, с блокировкой префикса.
021 Advisory-lock при снятии прав администратора, уникальный lower(username) (миграция 0008).
022 Контейнер не от root, healthcheck, блокировка миграций, requirements.lock.
023 Экранирование LIKE, журнал отказов очистки, заголовки безопасности, учёт force-удаления,
отзыв токенов при смене пароля (claim pv, миграция 0005).
024 Исправление находок ревью 011-023 (docs/reviews/2026-09-26-changes-011-023-review.md):
сериализация попыток входа, запрет переноса адресов в адрес сети/broadcast, журнал входов,
запрет смены своего пароля через PATCH, валидация PATCH устройства, обновлён тест токенов.
Тесты: 14 passed. Документация: README.md, docs/changes/011-024, docs/reviews.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cd09ef0805
commit
13e17fbb47
57 files changed
+1748
-166
No files matched your search
+37
-3
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
|
||||
@@ -10,20 +11,26 @@ from sqlalchemy import select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from app.api.v1 import auth, journal, overview, prefixes, refs, users
|
||||
from app.config import settings
|
||||
from app.config import settings, validate_secrets
|
||||
from app.db import SessionLocal
|
||||
from app.models import DeviceType, Role, User
|
||||
from app.request_context import RequestContextMiddleware
|
||||
from app.rotation import rotation_loop
|
||||
from app.security import hash_password
|
||||
|
||||
validate_secrets() # приложение не стартует с небезопасной конфигурацией (изменение 017)
|
||||
|
||||
log = logging.getLogger("ipam")
|
||||
DEFAULT_TYPES = ["Сервер", "Сетевое оборудование", "Сетевое хранилище", "Рабочая станция", "Другое"]
|
||||
|
||||
|
||||
def seed():
|
||||
with SessionLocal() as db:
|
||||
if not db.scalar(select(User.id).limit(1)) and settings.admin_password:
|
||||
db.add(User(username=settings.admin_username, password_hash=hash_password(settings.admin_password), role=Role.admin))
|
||||
if not db.scalar(select(User.id).limit(1)):
|
||||
if settings.admin_password:
|
||||
db.add(User(username=settings.admin_username, password_hash=hash_password(settings.admin_password), role=Role.admin))
|
||||
else:
|
||||
log.warning("В БД нет пользователей и ADMIN_PASSWORD не задан: администратор не создан, войти в UI нельзя")
|
||||
if not db.scalar(select(DeviceType.id).limit(1)):
|
||||
db.add_all(DeviceType(name=n, is_default=(n == "Другое")) for n in DEFAULT_TYPES)
|
||||
db.commit()
|
||||
@@ -37,8 +44,35 @@ async def lifespan(_: FastAPI):
|
||||
task.cancel()
|
||||
|
||||
|
||||
CSP = "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; font-src 'self'; frame-ancestors 'none'"
|
||||
DOCS_PATHS = ("/docs", "/redoc", "/openapi.json") # Swagger UI грузит ресурсы с CDN — CSP для него не ставим
|
||||
|
||||
|
||||
class SecurityHeadersMiddleware:
|
||||
"""ASGI-middleware: заголовки безопасности на все ответы (изменение 023)."""
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
async def __call__(self, scope, receive, send):
|
||||
if scope["type"] != "http":
|
||||
return await self.app(scope, receive, send)
|
||||
docs = scope["path"].startswith(DOCS_PATHS)
|
||||
|
||||
async def send_with_headers(message):
|
||||
if message["type"] == "http.response.start":
|
||||
extra = [(b"x-content-type-options", b"nosniff"), (b"referrer-policy", b"no-referrer")]
|
||||
if not docs:
|
||||
extra += [(b"content-security-policy", CSP.encode()), (b"x-frame-options", b"DENY")]
|
||||
message["headers"] = [*message.get("headers", []), *extra]
|
||||
await send(message)
|
||||
|
||||
await self.app(scope, receive, send_with_headers)
|
||||
|
||||
|
||||
app = FastAPI(title="IPAM Manager API", version="1.0.0", lifespan=lifespan)
|
||||
app.add_middleware(RequestContextMiddleware)
|
||||
app.add_middleware(SecurityHeadersMiddleware)
|
||||
|
||||
api = APIRouter(prefix="/api/v1")
|
||||
for r in (auth.router, overview.router, refs.router, prefixes.router, journal.router, users.router):
|
||||
|
||||
Reference in new issue
Block a user