189 lines
9.8 KiB
Markdown
189 lines
9.8 KiB
Markdown
# План разработки — IP WhiteList Microservice v2
|
||
|
||
> **Автор:** GitHub Copilot (Claude Sonnet 4.6)
|
||
> **Дата:** 2026-05-29
|
||
|
||
---
|
||
|
||
## Стек
|
||
|
||
| Слой | Технология | Обоснование |
|
||
|---|---|---|
|
||
| Бэкенд | Python 3.11+ / FastAPI | Коллеги знают Python, авто-документация |
|
||
| БД | PostgreSQL | Надёжно, поддерживает аудит и сложные запросы |
|
||
| Работа с БД | psycopg2 + сырой SQL | Проще чем ORM, понятно всем, никакой магии |
|
||
| Миграции | Alembic | Только для версионирования схемы |
|
||
| Фронтенд | Jinja2 + обычные HTML-формы | Без JS-фреймворков, минимум зависимостей |
|
||
| Авторизация | Keycloak OIDC (JWT) | Заглушка только в dev через `.env` флаг |
|
||
| IP-логика | stdlib `ipaddress` + `netaddr` | Суммаризация CIDR через `netaddr` |
|
||
|
||
---
|
||
|
||
## Открытые вопросы (нужно прояснить до кодирования)
|
||
|
||
1. **Чекбокс администратора** — ТЗ: admin = `clientId == WZ01112` + «отдельный чек-бокс». Что это: отдельный claim в Keycloak-токене (`is_admin: true`)? Роль? Нужно уточнить у команды Keycloak.
|
||
2. **Создание Company в БД** — когда появляется запись: при первом входе пользователя автоматически, или администратор заводит вручную?
|
||
3. **Кто потребляет внешний endpoint** — endpoint без авторизации, доступ по IP. Список доверенных IP задаётся конфигом? Nginx ACL?
|
||
|
||
---
|
||
|
||
## Файловая структура
|
||
|
||
```
|
||
IPWhiteList/
|
||
├── app/
|
||
│ ├── main.py # FastAPI app, роутеры, startup
|
||
│ ├── config.py # Настройки из .env (DEFAULT_LIMIT, DEV_MODE, DB_DSN и др.)
|
||
│ ├── db.py # psycopg2 connection pool
|
||
│ ├── models/
|
||
│ │ └── sql.py # DDL-схема (только для документации, не ORM)
|
||
│ ├── validators.py # IPv4/CIDR: формат, маска, серые адреса, нормализация
|
||
│ ├── crud/
|
||
│ │ ├── entries.py # CRUD whitelist_entries
|
||
│ │ ├── companies.py # Компании и лимиты
|
||
│ │ └── audit.py # Запись в audit_log
|
||
│ ├── auth/
|
||
│ │ ├── oidc.py # Валидация JWT Keycloak
|
||
│ │ ├── stub.py # Dev-заглушка (только при DEV_MODE=true)
|
||
│ │ └── deps.py # FastAPI Depends: current_user
|
||
│ ├── routers/
|
||
│ │ ├── entries.py # CRUD UI-роуты + HTMX-фрагменты
|
||
│ │ ├── admin.py # Аудит, лимиты (только admin)
|
||
│ │ └── external.py # GET /api/v1/export — txt-файл
|
||
│ └── cidr_utils.py # Суммаризация через netaddr
|
||
├── templates/
|
||
│ ├── base.html
|
||
│ ├── index.html # Таблица записей + индикатор лимита
|
||
│ ├── partials/
|
||
│ │ ├── table.html # HTMX-фрагмент таблицы
|
||
│ │ └── form.html # Форма создания/редактирования
|
||
│ └── admin/
|
||
│ ├── audit.html
|
||
│ └── limits.html
|
||
├── static/
|
||
│ └── style.css
|
||
├── migrations/
|
||
│ ├── env.py
|
||
│ └── versions/
|
||
├── tests/
|
||
│ ├── test_validators.py # Юниты для IPv4-валидации (критично!)
|
||
│ └── test_crud.py
|
||
├── docs/
|
||
│ ├── plan.md # LEGACY
|
||
│ ├── plan-v2.md # Этот файл
|
||
│ └── WhiteIPlist.docx # Исходное ТЗ
|
||
├── .env.example
|
||
├── alembic.ini
|
||
├── docker-compose.yml # PostgreSQL для dev
|
||
├── requirements.txt
|
||
└── README.md
|
||
```
|
||
|
||
---
|
||
|
||
## Схема БД
|
||
|
||
```sql
|
||
-- Компании (создаются автоматически при первом входе или вручную админом — уточнить)
|
||
CREATE TABLE companies (
|
||
id SERIAL PRIMARY KEY,
|
||
client_id VARCHAR(64) UNIQUE NOT NULL, -- из Keycloak claim
|
||
name VARCHAR(255),
|
||
custom_limit INTEGER DEFAULT NULL -- NULL = использовать глобальный DEFAULT_LIMIT
|
||
);
|
||
|
||
-- Whitelist-записи
|
||
CREATE TABLE whitelist_entries (
|
||
id SERIAL PRIMARY KEY,
|
||
company_id INTEGER NOT NULL REFERENCES companies(id),
|
||
value CIDR NOT NULL, -- нормализованный CIDR
|
||
comment VARCHAR(255),
|
||
created_by VARCHAR(255) NOT NULL, -- email из токена
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_by VARCHAR(255),
|
||
updated_at TIMESTAMPTZ,
|
||
deleted_by VARCHAR(255),
|
||
deleted_at TIMESTAMPTZ -- NULL = активная запись
|
||
);
|
||
|
||
-- Аудит (только append, без UPDATE/DELETE)
|
||
CREATE TABLE audit_log (
|
||
id SERIAL PRIMARY KEY,
|
||
user_email VARCHAR(255) NOT NULL,
|
||
company_id INTEGER NOT NULL,
|
||
action VARCHAR(32) NOT NULL, -- CREATE | UPDATE | DELETE
|
||
old_value TEXT,
|
||
new_value TEXT,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
|
||
-- Индексы
|
||
CREATE INDEX ON whitelist_entries(company_id) WHERE deleted_at IS NULL;
|
||
CREATE INDEX ON audit_log(company_id);
|
||
```
|
||
|
||
---
|
||
|
||
## Этапы
|
||
|
||
### Этап 1 — Каркас + конфиг
|
||
- [ ] Структура папок
|
||
- [ ] `requirements.txt`: fastapi, uvicorn, psycopg2-binary, alembic, jinja2, python-jose, netaddr
|
||
- [ ] `.env.example` со всеми переменными: `DB_DSN`, `DEFAULT_LIMIT=15`, `DEV_MODE=false`, `KEYCLOAK_URL`, `KEYCLOAK_REALM`, `KEYCLOAK_CLIENT_ID`, `ALLOWED_EXPORT_IPS`
|
||
- [ ] `config.py` — читает `.env`, все параметры типизированы
|
||
- [ ] `db.py` — psycopg2 connection pool (SimpleConnectionPool)
|
||
- [ ] `docker-compose.yml` с PostgreSQL
|
||
|
||
### Этап 2 — Миграции (схема БД)
|
||
- [ ] Alembic init
|
||
- [ ] Initial migration: `companies`, `whitelist_entries`, `audit_log` + индексы
|
||
- [ ] Проверка `alembic upgrade head`
|
||
|
||
### Этап 3 — Валидатор IPv4 (с тестами)
|
||
- [ ] `validators.py`: принимает строку → возвращает нормализованный CIDR или ошибку
|
||
- [ ] Проверка формата: одиночный IP или CIDR
|
||
- [ ] Проверка маски: /22 – /32 (шире /21 — `ValidationError`)
|
||
- [ ] Нормализация host-битов: `192.168.1.5/24` → `192.168.1.0/24` + флаг `was_normalized=True`
|
||
- [ ] Запрет серых диапазонов (все из Приложения А ТЗ)
|
||
- [ ] `tests/test_validators.py` — покрыть все граничные случаи
|
||
|
||
### Этап 4 — CRUD-логика
|
||
- [ ] `crud/companies.py`: get_or_create по client_id, get_limit (custom_limit ?? DEFAULT_LIMIT)
|
||
- [ ] `crud/entries.py`: список активных, создание (лимит + дубликаты + пересечения), редактирование, soft-delete
|
||
- [ ] `crud/audit.py`: append-only запись
|
||
|
||
### Этап 5 — Авторизация
|
||
- [ ] `auth/oidc.py` — валидация JWT через JWKS Keycloak, извлечение `clientID`, `email`, определение роли
|
||
- [ ] Логика роли admin: `clientID == WZ01112` + (claim `is_admin == true` — **уточнить**)
|
||
- [ ] `auth/stub.py` — только при `DEV_MODE=true`: читает `X-Dev-User` из заголовка
|
||
- [ ] `auth/deps.py` — `Depends(current_user)` для роутеров
|
||
|
||
### Этап 6 — Роутеры + UI
|
||
- [ ] `routers/entries.py`: список, форма создания, форма редактирования, удаление, переключатель компании
|
||
- [ ] `routers/admin.py`: журнал аудита, управление лимитами
|
||
- [ ] Шаблоны Jinja2: base.html, index.html, form.html, admin/audit.html, admin/limits.html
|
||
- [ ] Индикатор лимита «X из N» на странице
|
||
- [ ] Уведомление о нормализации адреса пользователю
|
||
|
||
### Этап 7 — Внешний endpoint
|
||
- [ ] `GET /api/v1/export` — только активные записи всех компаний
|
||
- [ ] Суммаризация через `netaddr.cidr_merge()`
|
||
- [ ] Ответ: `text/plain`, одна строка — один CIDR
|
||
- [ ] IP-фильтр из `ALLOWED_EXPORT_IPS` (middleware или Depends)
|
||
|
||
### Этап 8 — Деплой
|
||
- [ ] `Dockerfile` (python:3.11-slim, uvicorn)
|
||
- [ ] Systemd unit как альтернатива
|
||
- [ ] Nginx конфиг: reverse proxy + location для static
|
||
- [ ] README: как поднять с нуля
|
||
|
||
---
|
||
|
||
## Ключевые принципы
|
||
|
||
- **Синхронный код везде** — никакого async/await. FastAPI поддерживает синхронные роутеры.
|
||
- **Серверная валидация — авторитетная**. Клиентская — только UX.
|
||
- **`DEV_MODE=true`** — единственный способ обойти Keycloak. В prod недоступен.
|
||
- **Audit log — append only**. Никаких UPDATE/DELETE в `audit_log`.
|
||
- **Лимит `DEFAULT_LIMIT`** — всегда из `config.py`, который читает `.env`. Без пересборки.
|