fix: seed --dev только при DEBUG=true (OQ-60, #9 п.1)
CLI `python -m scripts.seed --dev` сверяется с `DEBUG` (`check_dev_allowed`) до чтения seed и подключения к БД; при `DEBUG=false` — код возврата 2 и сообщение «--dev доступен только при DEBUG=true (dev-пользователи не для прода)». `run_seed(session, dev=True)` защиты не имеет (её вызывают тесты). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
parent
7e550246b5
commit
5e11f0d0bf
@ -74,7 +74,7 @@
|
||||
(`LAST_ADMIN` с блокировкой строки права). Порядок: маршрут (`require(code)`) → policy → сервис → аудит.
|
||||
- **Аутентификация** — `core/auth.py` (проверка JWT), `src/api/dependencies/auth.py` (`current_user`, `require(code)`);
|
||||
dev-эмитент (`POST /api/v1/dev/token`) существует только при `AUTH_DEV_ISSUER=true` **и** `DEBUG=true`.
|
||||
- **Seed** — `python -m scripts.seed [--dev]` (`make seed|seed-dev`), идемпотентен; `--dev` — только для разработки.
|
||||
- **Seed** — `python -m scripts.seed [--dev]` (`make seed|seed-dev`), идемпотентен; `--dev` — только для разработки (CLI отказывает с кодом 2 при `DEBUG=false`).
|
||||
**Сверка ORM↔DBML** — `scripts/check_dbml_vs_orm.py` (`make dbml-check`).
|
||||
|
||||
## Зависимости от соседей
|
||||
|
||||
10
README.md
10
README.md
@ -87,7 +87,9 @@ make seed-dev # справочники продукта + dev-пользов
|
||||
```
|
||||
|
||||
`make seed` загружает только справочники (роли, права, филиалы, план счетов, правила элиминации, чек-лист), без
|
||||
dev-пользователей. Seed идемпотентен: повторный запуск обновляет данные и ничего не дублирует.
|
||||
dev-пользователей. Seed идемпотентен: повторный запуск обновляет данные и ничего не дублирует. `make seed-dev`
|
||||
(`--dev`) работает только при `DEBUG=true` в `api/.env`; при `DEBUG=false` загрузчик отказывает с кодом 2
|
||||
(«`--dev` доступен только при DEBUG=true (dev-пользователи не для прода)»), не трогая БД.
|
||||
|
||||
### 5. Запуск
|
||||
|
||||
@ -482,7 +484,7 @@ email без учёта регистра проверяет только сер
|
||||
| `make up` | поднимает compose из `../docs/dev` (PostgreSQL, NATS, MinIO, бакет) |
|
||||
| `make migrate` | `alembic upgrade head` на БД из `api/.env` |
|
||||
| `make seed` | загрузка справочников продукта (идемпотентна) |
|
||||
| `make seed-dev` | то же и dev-пользователи/филиалы (`--dev`) — только для разработки |
|
||||
| `make seed-dev` | то же и dev-пользователи/филиалы (`--dev`) — только для разработки, требует `DEBUG=true` |
|
||||
| `make lint` | `ruff check`, `ruff format --check`, `mypy src scripts` |
|
||||
| `make test` | юнит-тесты: `pytest -m "not integration"`, Docker не нужен |
|
||||
| `make test-integration` | интеграционные тесты: `pytest -m integration`, нужны PostgreSQL (и NATS) |
|
||||
@ -579,8 +581,8 @@ Dockerfile только для локального compose
|
||||
- Маршрут существует лишь при `AUTH_DEV_ISSUER=true` **и** `DEBUG=true`; при любом другом сочетании его нет вовсе
|
||||
(404), а dev-проверяющий токенов не создаётся даже при заданном `AUTH_DEV_SECRET`. В проде `DEBUG=false`, поэтому
|
||||
dev-эмитент там невозможен.
|
||||
- `AUTH_DEV_SECRET` из `api/.env.example` — публичный пример, в проде не использовать. `make seed-dev` в прод не
|
||||
запускать.
|
||||
- `AUTH_DEV_SECRET` из `api/.env.example` — публичный пример, в проде не использовать. `make seed-dev`
|
||||
(`python -m scripts.seed --dev`) при `DEBUG=false` отказывает (код 2), dev-пользователи в прод не попадут.
|
||||
- Порты compose по умолчанию публикуются только на `127.0.0.1` (`BIND_ADDR`); пароли `armka`/`minioadmin` — только dev.
|
||||
|
||||
## Соседние документы
|
||||
|
||||
@ -26,9 +26,10 @@ ASSUMPTION(OQ-46)): `seed_lib/loaders_rules.py` проверяет, что ка
|
||||
Dev-данные (`--dev`, `seed/dev/`, задача 15): 2 региональных филиала (`regional_offices.yaml`, upsert по `name`)
|
||||
и по одному пользователю на каждую системную роль (`users.yaml`, `<роль>@armka.local`, upsert `app_user` по `email`,
|
||||
`user_role` по (user_id, role_id)). Ссылки пользователей на роли и филиалы проверяются до записи. Без `--dev` ни
|
||||
пользователи, ни dev-филиалы не загружаются. ASSUMPTION: dev-данные — только для локальной разработки и тестовых
|
||||
стендов; в прод `--dev` не запускать (защиты флага нет, спека её не требует). Удалений нет: роль, убранная из
|
||||
`users.yaml`, остаётся назначенной.
|
||||
пользователи, ни dev-филиалы не загружаются. Dev-данные — только для локальной разработки и тестовых стендов: CLI
|
||||
принимает `--dev` лишь при `DEBUG=true` (`check_dev_allowed`, OQ-60, issue #9), иначе — код возврата 2 до чтения
|
||||
seed и подключения к БД. `run_seed(session, dev=True)` защиты не имеет (её вызывают тесты). Удалений нет: роль,
|
||||
убранная из `users.yaml`, остаётся назначенной.
|
||||
|
||||
Расширение (задачи 13–15): поле в `SeedData` (`seed_lib/model.py`), читатель в `load_seed`, шаг в `apply_seed`.
|
||||
"""
|
||||
@ -37,14 +38,41 @@ import argparse
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||
|
||||
from scripts.seed_lib.apply import apply_seed
|
||||
from scripts.seed_lib.loaders import DEFAULT_SEED_DIR, load_seed
|
||||
from scripts.seed_lib.model import SeedData, SeedError, SeedReport
|
||||
from scripts.seed_lib.model import SeedError, SeedReport
|
||||
|
||||
__all__ = ["DEFAULT_SEED_DIR", "SeedError", "SeedReport", "apply_seed", "load_seed", "main", "run_seed"]
|
||||
if TYPE_CHECKING:
|
||||
from src.core.config import Settings
|
||||
|
||||
__all__ = [
|
||||
"DEFAULT_SEED_DIR",
|
||||
"DEV_FORBIDDEN_MESSAGE",
|
||||
"SeedError",
|
||||
"SeedReport",
|
||||
"apply_seed",
|
||||
"check_dev_allowed",
|
||||
"load_seed",
|
||||
"main",
|
||||
"run_seed",
|
||||
]
|
||||
|
||||
DEV_FORBIDDEN_MESSAGE = "--dev доступен только при DEBUG=true (dev-пользователи не для прода)"
|
||||
#: код возврата CLI при `--dev` без DEBUG (1 — ошибка файлов seed)
|
||||
EXIT_DEV_FORBIDDEN = 2
|
||||
|
||||
|
||||
class DevSeedForbiddenError(Exception):
|
||||
"""`--dev` запрошен при `DEBUG=false`."""
|
||||
|
||||
|
||||
def check_dev_allowed(settings: "Settings") -> str | None:
|
||||
"""Можно ли грузить dev-данные: `None` — да, иначе текст отказа (dev-пользователи только при `DEBUG=true`)."""
|
||||
return None if settings.DEBUG else DEV_FORBIDDEN_MESSAGE
|
||||
|
||||
|
||||
async def run_seed(session: AsyncSession, *, dev: bool = False, seed_dir: Path | None = None) -> SeedReport:
|
||||
@ -55,9 +83,15 @@ async def run_seed(session: AsyncSession, *, dev: bool = False, seed_dir: Path |
|
||||
return await apply_seed(session, load_seed(seed_dir, dev=dev))
|
||||
|
||||
|
||||
async def _run_cli(data: SeedData) -> SeedReport:
|
||||
# импорт здесь: настройки читаются из ENV_FILE только при реальном запуске,
|
||||
# поэтому `--help` и ошибки валидации работают без env
|
||||
async def _run_cli(seed_dir: Path, *, dev: bool) -> SeedReport:
|
||||
# импорт настроек — только после проверки файлов seed: модуль конфигурации читает ENV_FILE при импорте,
|
||||
# поэтому `--help` и ошибки валидации без `--dev` работают без env; `--dev` сначала сверяется с DEBUG
|
||||
if dev:
|
||||
from src.core.config import get_settings # noqa: PLC0415
|
||||
|
||||
if (refusal := check_dev_allowed(get_settings())) is not None:
|
||||
raise DevSeedForbiddenError(refusal)
|
||||
data = load_seed(seed_dir, dev=dev)
|
||||
from src.core.config import settings # noqa: PLC0415
|
||||
|
||||
engine = create_async_engine(
|
||||
@ -80,8 +114,10 @@ def main(argv: list[str] | None = None) -> int:
|
||||
parser.add_argument("--seed-dir", type=Path, default=DEFAULT_SEED_DIR, help="каталог seed (по умолчанию api/seed)")
|
||||
args = parser.parse_args(argv)
|
||||
try:
|
||||
data = load_seed(args.seed_dir, dev=args.dev)
|
||||
report = asyncio.run(_run_cli(data))
|
||||
report = asyncio.run(_run_cli(args.seed_dir, dev=args.dev))
|
||||
except DevSeedForbiddenError as error:
|
||||
print(f"Ошибка seed: {error}", file=sys.stderr)
|
||||
return EXIT_DEV_FORBIDDEN
|
||||
except SeedError as error:
|
||||
print(f"Ошибка seed: {error}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
@ -1,13 +1,17 @@
|
||||
"""Dev-seed (spec reference-data-seed, требование «Dev-данные только по явному флагу», задача 15)."""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from scripts.seed import SeedError, load_seed, run_seed
|
||||
from scripts.seed import DEV_FORBIDDEN_MESSAGE, SeedError, check_dev_allowed, load_seed, run_seed
|
||||
from src.core.config import get_settings
|
||||
from src.models.access import AppUser, Permission, Role, RolePermission, UserRole
|
||||
from src.models.directories import RegionalOffice
|
||||
from src.models.enums import BusinessSegment
|
||||
@ -240,3 +244,31 @@ def test_missing_dev_file_is_seed_error_only_with_dev_flag(tmp_path: Path) -> No
|
||||
load_seed(seed_dir)
|
||||
with pytest.raises(SeedError, match="users.yaml"):
|
||||
load_seed(seed_dir, dev=True)
|
||||
|
||||
|
||||
# ---- `--dev` только при DEBUG=true (OQ-60) ----
|
||||
|
||||
|
||||
def test_dev_allowed_only_with_debug() -> None:
|
||||
settings = get_settings()
|
||||
|
||||
assert check_dev_allowed(settings.model_copy(update={"DEBUG": True})) is None
|
||||
assert check_dev_allowed(settings.model_copy(update={"DEBUG": False})) == DEV_FORBIDDEN_MESSAGE
|
||||
assert "DEBUG=true" in DEV_FORBIDDEN_MESSAGE
|
||||
|
||||
|
||||
def test_cli_dev_without_debug_exits_with_code_2_before_db() -> None:
|
||||
"""`--dev` при DEBUG=false: код 2 и текст в stderr; до БД дело не доходит (переменная окружения важнее ENV_FILE)."""
|
||||
env = {**os.environ, "ENV_FILE": ".env.test", "DEBUG": "false"}
|
||||
result = subprocess.run( # noqa: S603
|
||||
[sys.executable, "-m", "scripts.seed", "--dev"],
|
||||
cwd=API_DIR,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
env=env,
|
||||
)
|
||||
|
||||
assert result.returncode == 2 # noqa: PLR2004
|
||||
assert DEV_FORBIDDEN_MESSAGE in result.stderr
|
||||
assert "Seed применён" not in result.stdout
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user