fix: P0/P1 fixes (auth+limit+schema) + project audit docs
This commit is contained in:
@@ -0,0 +1,242 @@
|
|||||||
|
Техническое задание
|
||||||
|
Микросервис управления доверенными адресами клиентов
|
||||||
|
Self-service портал для указания клиентами доверенных IPv4-адресов и подсетей,
|
||||||
|
исключаемых из блокировки на стороне облачного провайдера во время DDoS-атак
|
||||||
|
|
||||||
|
# 1. Назначение и цели
|
||||||
|
|
||||||
|
## 1.1. Назначение
|
||||||
|
|
||||||
|
Микросервис предоставляет клиентам облачного провайдера web-интерфейс для самостоятельного управления списком доверенных IPv4-адресов и подсетей. Записи из этого списка исключаются из автоматической блокировки сетевого взаимодействия системами фильтрации и митигации провайдера, что снижает количество ложноположительных срабатываний для легитимного трафика клиента.
|
||||||
|
|
||||||
|
> **Реализация:** `server.js` — точка входа, подключает все роуты. `ui/index.js` — UI-слой.
|
||||||
|
|
||||||
|
## 1.2. Цели
|
||||||
|
|
||||||
|
- Дать клиентам возможность самостоятельно поддерживать актуальный список доверенных IPv4-адресов, которые будут исключаться из фильтрации во время DDoS-атак.
|
||||||
|
|
||||||
|
> **Реализация:** `ui/routes/entries.js` — UI CRUD. `src/api/routes/entries.js` — REST API.
|
||||||
|
|
||||||
|
- Предоставить сетевым инженерам единую точку просмотра и управления списками доверенных клиентских белых IPv4-адресов.
|
||||||
|
|
||||||
|
> **Реализация:** `ui/routes/admin.js` — /admin и /audit UI. `src/api/routes/admin.js` — API /companies, /audit.
|
||||||
|
|
||||||
|
- Обеспечить машиночитаемую выдачу агрегированного (суммаризированного) списка для систем фильтрации трафика.
|
||||||
|
|
||||||
|
> **Реализация:** `src/routes/export.js` — GET /export. `src/validators.js` строки 125–181 — функция `aggregateCIDRs`.
|
||||||
|
|
||||||
|
# 2. Объем работ
|
||||||
|
|
||||||
|
- Web-страница / закладка в личном кабинете для управления whitelist-записями.
|
||||||
|
|
||||||
|
> **Реализация:** `views/index.ejs` — главная страница. `ui/routes/entries.js` — роуты GET /, POST /add, POST /edit/:id, POST /delete/:id.
|
||||||
|
|
||||||
|
- Авторизация через существующий экземпляр Keycloak (OIDC).
|
||||||
|
|
||||||
|
> **Реализация:** `src/auth.js` строки 220–260 — верификация JWT RS256 токена Keycloak. Строка 245: чтение claim `ClientID`. Строка 251: определение роли `isAdmin`.
|
||||||
|
|
||||||
|
- Валидация формы на стороне клиента и сервера.
|
||||||
|
|
||||||
|
> **Реализация:** `src/validators.js` строки 26–79 — серверная валидация `validate()`. `views/index.ejs` — атрибут `pattern` в поле ввода (клиентская).
|
||||||
|
|
||||||
|
- Внешний endpoint выдачи агрегированного списка. Выдача txt-файлом с переносом строки. Одна строка – один объект.
|
||||||
|
|
||||||
|
> **Реализация:** `src/routes/export.js` строки 27–64 — GET /export, Content-Type: text/plain, aggregateCIDRs → join('\n').
|
||||||
|
|
||||||
|
- Хранение записей, журнал аудита.
|
||||||
|
|
||||||
|
> **Реализация:** `sql/schema.sql` — таблицы `whitelist_entries`, `audit_log`. `src/queries.js` строки 211–230 — функция `logAudit`.
|
||||||
|
|
||||||
|
- Административное управление лимитами по компаниям.
|
||||||
|
|
||||||
|
> **Реализация:** `src/api/routes/admin.js` строки 27–35 — PATCH /companies/:id/limit. `src/queries.js` — функция `setLimit`. `views/admin.ejs` — UI формы лимитов.
|
||||||
|
|
||||||
|
# 3. Роли и права доступа
|
||||||
|
|
||||||
|
Роли определяются на основании claims в OIDC-токене Keycloak. Соответствие claim → роль настраивается на этапе развёртывания.
|
||||||
|
|
||||||
|
> **Реализация:** `src/auth.js` строка 31: `ADMIN_CLIENT_ID = process.env.ADMIN_CLIENT_ID || 'WZ01112'`. Строка 251: `isAdmin: clientId === ADMIN_CLIENT_ID`.
|
||||||
|
|
||||||
|
| Роль | Идентификация | Видимость записей | Права на изменение |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Клиент (client) | clientId | Только записи компаний, к которым принадлежит пользователь. | Создание, редактирование и удаление записей своих компаний (в пределах лимита). |
|
||||||
|
| Администратор (admin) | clientId = WZ01112 (Нубес) и отдельный чек-бокс | Записи всех компаний. | Создание, редактирование, удаление всех записей. Изменение лимита для отдельных компаний. |
|
||||||
|
|
||||||
|
> **Реализация ролей в API:** `src/api/routes/entries.js` — функция `resolveCompany()`: если `req.user.isAdmin && req.query.company` → берёт чужую компанию, иначе `getOrCreateCompany(clientId)`. `src/auth.js` строки 260–265 — middleware `requireAdmin`.
|
||||||
|
|
||||||
|
## 3.1. Принадлежность к компании
|
||||||
|
|
||||||
|
Принадлежность пользователя к компании определяется из claim токена. Поддерживается сценарий, когда пользователь принадлежит нескольким компаниям: в этом случае в интерфейсе предусматривается переключатель активной компании, а все операции выполняются в контексте выбранной компании.
|
||||||
|
|
||||||
|
> **Реализация:** `src/auth.js` строка 245: `clientId = payload.ClientID`. `src/queries.js` строки 6–15: `getOrCreateCompany(clientId, companyName)` — атомарный upsert.
|
||||||
|
>
|
||||||
|
> ⚠️ **Сценарий нескольких компаний не реализован** — в реальном токене Nubes `ClientID` = одна строка, массива нет. Ждём уточнения от devops.
|
||||||
|
|
||||||
|
Ожидаемые claims (имена согласуются с командой Keycloak):
|
||||||
|
- `ClientID` — идентификатор компании → `src/auth.js` строка 245
|
||||||
|
- `email` — идентификация пользователя для аудита → `src/auth.js` строка 248
|
||||||
|
|
||||||
|
# 4. Функциональные требования
|
||||||
|
|
||||||
|
## 4.1. Просмотр списка записей
|
||||||
|
|
||||||
|
- Клиент видит таблицу записей активной компании; Администратор – записи всех компаний с фильтром по компании.
|
||||||
|
|
||||||
|
> **Реализация:** `ui/routes/entries.js` — GET /: для admin грузит `getAllCompanies()` + фильтр по `?company=<id>`. `views/index.ejs` — dropdown компаний для admin, таблица записей.
|
||||||
|
|
||||||
|
- Для каждой записи отображаются: значение (адрес/подсеть), комментарий (если есть), автор(email), дата создания, дата последнего изменения.
|
||||||
|
|
||||||
|
> **Реализация:** `views/index.ejs` строки 280–295 — колонки таблицы: `value_cidr`, `comment`, `created_by`, `created_at`, `updated_at`. Даты в timezone `Europe/Moscow`.
|
||||||
|
|
||||||
|
- Soft-deleted записи по умолчанию скрыты; для администратора предусмотрен фильтр для их отображения.
|
||||||
|
|
||||||
|
> **Реализация:** `src/queries.js` строки 25–31 — `listEntries(companyId, includeDeleted)`. SQL: `AND deleted_at IS NULL` когда `includeDeleted=false`.
|
||||||
|
>
|
||||||
|
> ⚠️ **Фильтр "показать удалённые" в UI не реализован** — `includeDeleted` всегда `false`.
|
||||||
|
|
||||||
|
- Отображается текущее использование лимита: «использовано X из N».
|
||||||
|
|
||||||
|
> **Реализация:** `src/api/routes/entries.js` строка 62: `res.json({ entries, limit, used: entries.length })`. `views/index.ejs` — блок статистики `<%= used %> / <%= limit %>`.
|
||||||
|
|
||||||
|
## 4.2. Создание записи
|
||||||
|
|
||||||
|
- Форма содержит поля: значение (IPv4-адрес или подсеть CIDR) и необязательный комментарий (до 255 символов).
|
||||||
|
|
||||||
|
> **Реализация:** `views/index.ejs` — форма POST /add с полями `value` и `comment`. Атрибут `maxlength="18"` на поле адреса.
|
||||||
|
|
||||||
|
- Значение проходит валидацию (см. раздел 5) на клиенте и обязательно повторно на сервере.
|
||||||
|
|
||||||
|
> **Реализация:** `src/validators.js` строки 26–79 — `validate(input)`. Вызывается в `src/queries.js` строка 33: `const { cidr, wasNormalized } = validate(rawValue)`.
|
||||||
|
|
||||||
|
- Перед сохранением проверяется: соблюдение лимита компании, отсутствие пересечений и дубликатов внутри компании, отсутствие принадлежности к запрещённым диапазонам.
|
||||||
|
|
||||||
|
> **Реализация:** `src/queries.js` строки 32–70 — `createEntry()`: блокировка строки компании (FOR UPDATE), проверка лимита (строки 43–46), проверка дубликатов и пересечений (строки 49–57), проверка запрещённых диапазонов в `validate()` (строки 73–75 validators.js).
|
||||||
|
|
||||||
|
- При успешном сохранении создаётся запись аудита.
|
||||||
|
|
||||||
|
> **Реализация:** `src/queries.js` строка 66: `logAudit(userEmail, companyId, 'CREATE', null, cidr, ...)`.
|
||||||
|
|
||||||
|
## 4.3. Редактирование записи
|
||||||
|
|
||||||
|
- Редактирование значения и комментария доступно компании в рамках своих прав.
|
||||||
|
|
||||||
|
> **Реализация:** `ui/routes/entries.js` — POST /edit/:id. `src/api/routes/entries.js` — PATCH /entries/:id. Modal в `views/index.ejs` — кнопка `.btn-edit`, event delegation в `<script>`.
|
||||||
|
|
||||||
|
- При изменении значения повторно выполняется полный набор проверок валидации и пересечений.
|
||||||
|
|
||||||
|
> **Реализация:** `src/queries.js` строки 77–115 — `updateEntry()`: валидация через `validate()`, проверка пересечений (исключая саму запись: `AND id <> $2`).
|
||||||
|
|
||||||
|
- Изменение фиксируется в журнале аудита с сохранением прежнего и нового значения.
|
||||||
|
|
||||||
|
> **Реализация:** `src/queries.js` строка 107: `logAudit(userEmail, companyId, 'UPDATE', old.value_cidr, cidr, entryId, ...)`.
|
||||||
|
|
||||||
|
## 4.4. Удаление записи (soft delete)
|
||||||
|
|
||||||
|
- Удаление выполняется как логическое (soft delete): запись помечается удалённой (deleted_at, deleted_by), но физически сохраняется.
|
||||||
|
|
||||||
|
> **Реализация:** `src/queries.js` строки 118–145 — `deleteEntry()`: UPDATE SET `deleted_at = NOW(), deleted_by = userEmail`. `sql/schema.sql` — колонки `deleted_at`, `deleted_by` в таблице `whitelist_entries`.
|
||||||
|
|
||||||
|
- Удалённая запись освобождает место в лимите компании и исключается из внешней агрегированной выдачи.
|
||||||
|
|
||||||
|
> **Реализация:** `src/queries.js` строка 50: COUNT считает только `WHERE deleted_at IS NULL`. `src/routes/export.js` — запрос только активных записей.
|
||||||
|
|
||||||
|
- Действие фиксируется в журнале аудита.
|
||||||
|
|
||||||
|
> **Реализация:** `src/queries.js` строка 134: `logAudit(userEmail, companyId, 'DELETE', old.value_cidr, null, ...)`.
|
||||||
|
|
||||||
|
## 4.5. Лимит записей на компанию
|
||||||
|
|
||||||
|
- Действует глобальный лимит по умолчанию: 15 активных записей на компанию.
|
||||||
|
|
||||||
|
> **Реализация:** `src/queries.js` строки 17–20 — `getLimit()`: `parseInt(process.env.DEFAULT_LIMIT) || 15`.
|
||||||
|
|
||||||
|
- Значение глобального лимита по умолчанию задаётся конфигурацией сервиса и может быть изменено без пересборки.
|
||||||
|
|
||||||
|
> **Реализация:** `src/queries.js` строка 18: `process.env.DEFAULT_LIMIT` — переменная окружения, не хардкод.
|
||||||
|
|
||||||
|
- Для отдельной компании администратор может задать индивидуальный лимит, переопределяющий глобальный (как в большую, так и в меньшую сторону).
|
||||||
|
|
||||||
|
> **Реализация:** `src/api/routes/admin.js` строки 27–35 — PATCH /companies/:id/limit. `src/queries.js` строка 20: `company.custom_limit != null ? company.custom_limit : defaultLimit`. `sql/schema.sql` — колонка `custom_limit` в таблице `companies`.
|
||||||
|
|
||||||
|
- При попытке превысить лимит создание блокируется с понятным сообщением; в подсчёт идут только активные записи.
|
||||||
|
|
||||||
|
> **Реализация:** `src/queries.js` строки 43–46: `if (cnt >= limit) throw new Error('Лимит исчерпан: N из N')`. API возвращает 409.
|
||||||
|
|
||||||
|
- Снижение лимита ниже текущего числа записей не удаляет существующие записи, но блокирует создание новых до приведения в соответствие.
|
||||||
|
|
||||||
|
> **Реализация:** `src/api/routes/admin.js` строки 28–35 — setLimit просто записывает значение без удаления записей. Блокировка создания — через проверку `cnt >= limit` в `createEntry`.
|
||||||
|
|
||||||
|
## 4.6. Журнал аудита
|
||||||
|
|
||||||
|
- Все изменяющие операции фиксируются неизменяемыми записями аудита.
|
||||||
|
|
||||||
|
> **Реализация:** `src/queries.js` строки 211–218 — `logAudit()`: INSERT в `audit_log` без UPDATE/DELETE операций над ней.
|
||||||
|
|
||||||
|
- Каждая запись аудита содержит: кто (пользователь), когда (timestamp), компания, тип действия, прежнее и новое состояние.
|
||||||
|
|
||||||
|
> **Реализация:** `sql/schema.sql` — таблица `audit_log`: колонки `user_email`, `created_at`, `company_id`, `action`, `old_value`, `new_value`. `views/audit.ejs` строки 131–160 — отображение.
|
||||||
|
|
||||||
|
- Журнал доступен для просмотра только администратору.
|
||||||
|
|
||||||
|
> **Реализация:** `src/api/routes/admin.js` — GET /audit защищён `apiRequireAdmin`. `ui/routes/admin.js` — GET /audit проверяет `req.session.user.isAdmin`.
|
||||||
|
|
||||||
|
## 4.7. Внешняя выдача агрегированного списка
|
||||||
|
|
||||||
|
- Подсети суммаризируются (агрегируются в минимальный набор CIDR) по всем компаниям совместно. Пересечения между разными компаниями допустимы.
|
||||||
|
|
||||||
|
> **Реализация:** `src/validators.js` строки 125–181 — `aggregateCIDRs()`: сортировка, слияние перекрывающихся диапазонов, преобразование обратно в CIDR.
|
||||||
|
|
||||||
|
- Предоставляется отдельный HTTP GET endpoint, отдающий полный суммаризированный список активных записей всех компаний файлом в формате txt.
|
||||||
|
|
||||||
|
> **Реализация:** `src/routes/export.js` строки 27–64 — GET /export. Content-Type: `text/plain`, ответ: `aggregated.join('\n')`.
|
||||||
|
|
||||||
|
- Авторизация: на старте endpoint может работать без авторизации (по сетевому ограничению / разрешенный список потребителей по ip).
|
||||||
|
|
||||||
|
> ⚠️ **Не реализовано** — /export требует Bearer-токен (авторизован). По ТЗ должен быть доступен без авторизации по IP-списку.
|
||||||
|
|
||||||
|
# 5. Требования к валидации
|
||||||
|
|
||||||
|
Валидация выполняется на клиенте и обязательно дублируется на сервере. Серверная валидация является авторитетной.
|
||||||
|
|
||||||
|
> **Реализация:** `src/validators.js` — вся серверная валидация. `views/index.ejs` — атрибут `pattern` (клиент).
|
||||||
|
|
||||||
|
| Правило | Описание | Реализация |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Формат IPv4 | Допускается одиночный адрес или подсеть CIDR /22–/32. | `src/validators.js` строки 41–53: добавление /32 если нет маски, проверка `mask < 22 \|\| mask > 32`. |
|
||||||
|
| Только IPv4 | IPv6-значения или доменные имена отклоняются. | `src/validators.js` строки 31–36: `if (raw.includes(':'))` → отклонить, проверка букв. |
|
||||||
|
| Корректность подсети | Host-биты обнуляются, пользователь уведомляется о нормализации. | `src/validators.js` строки 55–69: битовая арифметика, `wasNormalized = addr !== networkAddr`. Флаш-сообщение в `ui/routes/entries.js`. |
|
||||||
|
| Запрет серых адресов | Диапазоны из Приложения А запрещены. | `src/validators.js` строки 4–20: `BLOCKED_RANGES[]`. Строки 73–75: проверка `overlaps()`. |
|
||||||
|
| Отсутствие дубликатов | Совпадающие записи в компании запрещены. | `src/queries.js` строка 53: `if (row.value_cidr === cidr) throw`. Уникальный индекс в `sql/schema.sql`. |
|
||||||
|
| Отсутствие пересечений | Пересечение с существующей записью в компании запрещено. | `src/queries.js` строки 49–57: перебор активных записей + `overlaps()`. Между компаниями — допускается. |
|
||||||
|
| Длина комментария | Не более 255 символов. | `src/validators.js` строка 183 (или в api/routes/entries.js): проверка `comment.length > 255` → 400. |
|
||||||
|
|
||||||
|
# Приложение А – Список запрещённых к созданию подсетей
|
||||||
|
|
||||||
|
> **Реализация:** `src/validators.js` строки 4–20 — массив `BLOCKED_RANGES`.
|
||||||
|
|
||||||
|
| Назначение | Префикс |
|
||||||
|
| --- | --- |
|
||||||
|
| Private (RFC1918) | 10.0.0.0/8 |
|
||||||
|
| Private (RFC1918) | 172.16.0.0/12 |
|
||||||
|
| Private (RFC1918) | 192.168.0.0/16 |
|
||||||
|
| CGNAT (RFC6598) | 100.64.0.0/10 |
|
||||||
|
| Loopback | 127.0.0.0/8 |
|
||||||
|
| Link-local (APIPA) | 169.254.0.0/16 |
|
||||||
|
| IANA special block | 192.0.0.0/24 |
|
||||||
|
| TEST-NET-1 (docs) | 192.0.2.0/24 |
|
||||||
|
| TEST-NET-2 (docs) | 198.51.100.0/24 |
|
||||||
|
| TEST-NET-3 (docs) | 203.0.113.0/24 |
|
||||||
|
| Benchmarking | 198.18.0.0/15 |
|
||||||
|
| Multicast | 224.0.0.0/4 |
|
||||||
|
| Reserved (Class E) | 240.0.0.0/4 |
|
||||||
|
| Limited broadcast | 255.255.255.255/32 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Расхождения с ТЗ (что не реализовано)
|
||||||
|
|
||||||
|
| Пункт ТЗ | Статус |
|
||||||
|
| --- | --- |
|
||||||
|
| 3.1 Несколько компаний для одного пользователя | Не реализовано — ждём формат claim от devops |
|
||||||
|
| 4.1 Фильтр soft-deleted записей для admin | Не реализовано — `includeDeleted` всегда false |
|
||||||
|
| 4.7 /export без авторизации (по IP) | Не реализовано — требует Bearer-токен |
|
||||||
+97
@@ -0,0 +1,97 @@
|
|||||||
|
Техническое задание
|
||||||
|
Микросервис управления доверенными адресами клиентов
|
||||||
|
Self-service портал для указания клиентами доверенных IPv4-адресов и подсетей,
|
||||||
|
исключаемых из блокировки на стороне облачного провайдера во время DDoS-атак
|
||||||
|
# 1. Назначение и цели
|
||||||
|
## 1.1. Назначение
|
||||||
|
Микросервис предоставляет клиентам облачного провайдера web-интерфейс для самостоятельного управления списком доверенных IPv4-адресов и подсетей. Записи из этого списка исключаются из автоматической блокировки сетевого взаимодействия системами фильтрации и митигации провайдера, что снижает количество ложноположительных срабатываний для легитимного трафика клиента.
|
||||||
|
## 1.2. Цели
|
||||||
|
- Дать клиентам возможность самостоятельно поддерживать актуальный список доверенных IPv4-адресов, которые будут исключаться из фильтрации во время DDoS-атак.
|
||||||
|
- Предоставить сетевым инженерам единую точку просмотра и управления списками доверенных клиентских белых IPv4-адресов.
|
||||||
|
- Обеспечить машиночитаемую выдачу агрегированного (суммаризированного) списка для систем фильтрации трафика.
|
||||||
|
# 2. Объем работ
|
||||||
|
- Web-страница / закладка в личном кабинете для управления whitelist-записями.
|
||||||
|
- Авторизация через существующий экземпляр Keycloak (OIDC).
|
||||||
|
- Валидация формы на стороне клиента и сервера.
|
||||||
|
- Внешний endpoint выдачи агрегированного списка. Выдача txt-файлом с переносом строки. Одна строка – один объект.
|
||||||
|
- Хранение записей, журнал аудита.
|
||||||
|
- Административное управление лимитами по компаниям.
|
||||||
|
# 3. Роли и права доступа
|
||||||
|
Роли определяются на основании claims в OIDC-токене Keycloak. Соответствие claim → роль настраивается на этапе развёртывания.
|
||||||
|
## 3.1. Принадлежность к компании
|
||||||
|
Принадлежность пользователя к компании определяется из claim токена. Поддерживается сценарий, когда пользователь принадлежит нескольким компаниям: в этом случае в интерфейсе предусматривается переключатель активной компании, а все операции выполняются в контексте выбранной компании.
|
||||||
|
Ожидаемые claims (имена согласуются с командой Keycloak):
|
||||||
|
clientID — идентификатор компании
|
||||||
|
email — идентификация пользователя для аудита
|
||||||
|
# 4. Функциональные требования
|
||||||
|
## 4.1. Просмотр списка записей
|
||||||
|
- Клиент видит таблицу записей активной компании; Администратор – записи всех компаний с фильтром по компании.
|
||||||
|
- Для каждой записи отображаются: значение (адрес/подсеть), комментарий (если есть), автор(email), дата создания, дата последнего изменения.
|
||||||
|
- Soft-deleted записи по умолчанию скрыты; для администратора предусмотрен фильтр для их отображения.
|
||||||
|
- Отображается текущее использование лимита: «использовано X из N».
|
||||||
|
## 4.2. Создание записи
|
||||||
|
- Форма содержит поля: значение (IPv4-адрес или подсеть CIDR) и необязательный комментарий (до 255 символов).
|
||||||
|
- Значение проходит валидацию (см. раздел 5) на клиенте и обязательно повторно на сервере.
|
||||||
|
- Перед сохранением проверяется: соблюдение лимита компании, отсутствие пересечений и дубликатов внутри компании, отсутствие принадлежности к запрещённым диапазонам.
|
||||||
|
- При успешном сохранении создаётся запись аудита.
|
||||||
|
## 4.3. Редактирование записи
|
||||||
|
- Редактирование значения и комментария доступно компании в рамках своих прав.
|
||||||
|
- При изменении значения повторно выполняется полный набор проверок валидации и пересечений.
|
||||||
|
- Изменение фиксируется в журнале аудита с сохранением прежнего и нового значения.
|
||||||
|
## 4.4. Удаление записи (soft delete)
|
||||||
|
- Удаление выполняется как логическое (soft delete): запись помечается удалённой (deleted_at, deleted_by), но физически сохраняется.
|
||||||
|
- Удалённая запись освобождает место в лимите компании и исключается из внешней агрегированной выдачи.
|
||||||
|
- Действие фиксируется в журнале аудита.
|
||||||
|
## 4.5. Лимит записей на компанию
|
||||||
|
- Действует глобальный лимит по умолчанию: 15 активных записей на компанию.
|
||||||
|
- Значение глобального лимита по умолчанию задаётся конфигурацией сервиса и может быть изменено без пересборки.
|
||||||
|
- Для отдельной компании администратор может задать индивидуальный лимит, переопределяющий глобальный (как в большую, так и в меньшую сторону).
|
||||||
|
- При попытке превысить лимит создание блокируется с понятным сообщением; в подсчёт идут только активные записи.
|
||||||
|
- Снижение лимита ниже текущего числа записей не удаляет существующие записи, но блокирует создание новых до приведения в соответствие.
|
||||||
|
## 4.6. Журнал аудита
|
||||||
|
- Все изменяющие операции фиксируются неизменяемыми записями аудита.
|
||||||
|
- Каждая запись аудита содержит: кто (пользователь), когда (timestamp), компания, тип действия, прежнее и новое состояние.
|
||||||
|
- Журнал доступен для просмотра только администратору.
|
||||||
|
## 4.7. Внешняя выдача агрегированного списка
|
||||||
|
- Подсети суммаризируются (агрегируются в минимальный набор CIDR) по всем компаниям совместно. Пересечения между разными компаниями допустимы.
|
||||||
|
- Предоставляется отдельный HTTP GET endpoint, отдающий полный суммаризированный список активных записей всех компаний файлом в формате txt.
|
||||||
|
- Авторизация: на старте endpoint может работать без авторизации (по сетевому ограничению / разрешенный список потребителей по ip).
|
||||||
|
# 5. Требования к валидации
|
||||||
|
Валидация выполняется на клиенте и обязательно дублируется на сервере. Серверная валидация является авторитетной.
|
||||||
|
|
||||||
|
# Приложение А – Список запрещенных к созданию подсетей.
|
||||||
|
|
||||||
|
|
||||||
|
| Роль | Идентификация | Видимость записей | Права на изменение |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Клиент (client) | clientId | Только записи компаний, к которым принадлежит пользователь. | Создание, редактирование и удаление записей своих компаний (в пределах лимита). |
|
||||||
|
| Администратор (admin) | clientId = WZ01112 (Нубес) и отдельный чек-бокс | Записи всех компаний. | Создание, редактирование, удаление всех записей. Изменение лимита для отдельных компаний. |
|
||||||
|
|
||||||
|
|
||||||
|
| Правило | Описание |
|
||||||
|
| --- | --- |
|
||||||
|
| Формат IPv4 | Допускается одиночный адрес (например 203.0.113.10) или подсеть в нотации CIDR (например 203.0.113.0/24). Допускается использование масок /32 - /22. Маска /21 и больше не допускается. |
|
||||||
|
| Только IPv4 | IPv6-значения или доменные имена отклоняются. |
|
||||||
|
| Корректность подсети | Введенный адрес с маской подсети должен нормализоваться к адресу подсети, все host-биты должны быть обнулены. Пользователь должен быть уведомлен, что ввел адрес из хостовой части, а не адрес подсети и произошла нормализация. |
|
||||||
|
| Запрет серых адресов | Адреса и подсети из частных диапазонов (Приложение А) запрещены к добавлению. |
|
||||||
|
| Отсутствие дубликатов | В пределах одной компании запрещены полностью совпадающие записи. |
|
||||||
|
| Отсутствие пересечений | В пределах одной компании запрещено добавление записи, пересекающейся с уже существующей (включая вложенность подсетей). Между разными компаниями пересечения допускаются. |
|
||||||
|
| Длина комментария | Не более 255 символов; поле необязательное. |
|
||||||
|
|
||||||
|
|
||||||
|
| Назначение | Префикс |
|
||||||
|
| --- | --- |
|
||||||
|
| Private (RFC1918) | 10.0.0.0/8 |
|
||||||
|
| Private (RFC1918) | 172.16.0.0/12 |
|
||||||
|
| Private (RFC1918) | 192.168.0.0/16 |
|
||||||
|
| CGNAT (RFC6598) | 100.64.0.0/10 |
|
||||||
|
| Loopback | 127.0.0.0/8 |
|
||||||
|
| Link-local (APIPA) | 169.254.0.0/16 |
|
||||||
|
| IANA special block | 192.0.0.0/24 |
|
||||||
|
| TEST-NET-1 (docs) | 192.0.2.0/24 |
|
||||||
|
| TEST-NET-2 (docs) | 198.51.100.0/24 |
|
||||||
|
| TEST-NET-3 (docs) | 203.0.113.0/24 |
|
||||||
|
| Benchmarking | 198.18.0.0/15 |
|
||||||
|
| Multicast | 224.0.0.0/4 |
|
||||||
|
| Reserved (Class E) | 240.0.0.0/4 |
|
||||||
|
| Limited broadcast | 255.255.255.255/32 |
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Forbidden
|
||||||
|
Transaction ID: bdd7fbca-219a-4dbf-95ca-a27cc8bdecd9
|
||||||
+2
-2
@@ -29,8 +29,8 @@ function createApiRouter({ auth, q }) {
|
|||||||
|
|
||||||
// Все /api/v1/* требуют Bearer — 401 JSON если нет токена
|
// Все /api/v1/* требуют Bearer — 401 JSON если нет токена
|
||||||
router.use(requireBearer);
|
router.use(requireBearer);
|
||||||
// Валидация токена — переиспользуем auth.middleware из src/auth.js
|
// Валидация токена — Bearer-only (игнорирует сессию, всегда проверяет JWT)
|
||||||
router.use(auth.middleware);
|
router.use(auth.bearerMiddleware);
|
||||||
|
|
||||||
router.use('/entries', createEntriesRouter({ q }));
|
router.use('/entries', createEntriesRouter({ q }));
|
||||||
router.use('/', createAdminRouter({ q }));
|
router.use('/', createAdminRouter({ q }));
|
||||||
|
|||||||
@@ -26,15 +26,17 @@ function createAdminRouter({ q }) {
|
|||||||
|
|
||||||
// PATCH /api/v1/companies/:id/limit — установить лимит (admin only)
|
// PATCH /api/v1/companies/:id/limit — установить лимит (admin only)
|
||||||
router.patch('/companies/:id/limit', apiRequireAdmin, json, async (req, res) => {
|
router.patch('/companies/:id/limit', apiRequireAdmin, json, async (req, res) => {
|
||||||
const limit = parseInt((req.body || {}).limit, 10);
|
const rawLimit = (req.body || {}).limit;
|
||||||
if (isNaN(limit) || limit < 0) {
|
const isReset = rawLimit === null || rawLimit === undefined;
|
||||||
return res.status(400).json({ error: 'limit must be non-negative integer' });
|
const limit = isReset ? null : parseInt(rawLimit, 10);
|
||||||
|
if (!isReset && (isNaN(limit) || limit < 0)) {
|
||||||
|
return res.status(400).json({ error: 'limit must be non-negative integer or null to reset' });
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await q.setLimit(req.params.id, limit);
|
await q.setLimit(req.params.id, limit);
|
||||||
res.json({ ok: true, limit });
|
res.json({ ok: true, limit });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).json({ error: e.message });
|
res.status(e.status || 500).json({ error: e.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+21
-1
@@ -93,7 +93,8 @@ async function initAuth() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
isOidc,
|
isOidc,
|
||||||
middleware: createMiddleware(isOidc),
|
middleware: createMiddleware(isOidc),
|
||||||
|
bearerMiddleware: createBearerMiddleware(isOidc),
|
||||||
// OIDC-хелперы (используются в src/routes/auth.js)
|
// OIDC-хелперы (используются в src/routes/auth.js)
|
||||||
buildAuthUrl,
|
buildAuthUrl,
|
||||||
exchangeCode,
|
exchangeCode,
|
||||||
@@ -252,6 +253,25 @@ function userFromPayload(payload) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Bearer-only middleware (для /api/v1) ────────────────────────────────────
|
||||||
|
// Не смотрит на сессию — всегда проверяет Bearer JWT явно.
|
||||||
|
// Используется в src/api/index.js вместо auth.middleware.
|
||||||
|
function createBearerMiddleware(isOidc) {
|
||||||
|
return function bearerAuthMiddleware(req, res, next) {
|
||||||
|
const bearer = (req.headers.authorization || '').replace(/^Bearer\s+/i, '').trim();
|
||||||
|
if (!bearer) {
|
||||||
|
return res.status(401).json({ error: 'Bearer token required' });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const payload = isOidc ? verifyOidcToken(bearer) : verifyMockToken(bearer);
|
||||||
|
req.user = userFromPayload(payload);
|
||||||
|
return next();
|
||||||
|
} catch (e) {
|
||||||
|
return res.status(401).json({ error: 'Invalid token: ' + e.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// ── requireAdmin middleware ──────────────────────────────────────────────────
|
// ── requireAdmin middleware ──────────────────────────────────────────────────
|
||||||
/**
|
/**
|
||||||
* Middleware-защита для admin-only маршрутов.
|
* Middleware-защита для admin-only маршрутов.
|
||||||
|
|||||||
+6
-1
@@ -187,10 +187,15 @@ async function getAllCompanies() {
|
|||||||
* @param {number|null} newLimit — новый лимит или null для сброса
|
* @param {number|null} newLimit — новый лимит или null для сброса
|
||||||
*/
|
*/
|
||||||
async function setLimit(companyId, newLimit) {
|
async function setLimit(companyId, newLimit) {
|
||||||
await pool.query(
|
const result = await pool.query(
|
||||||
'UPDATE companies SET custom_limit = $1, updated_at = NOW() WHERE id = $2',
|
'UPDATE companies SET custom_limit = $1, updated_at = NOW() WHERE id = $2',
|
||||||
[newLimit, companyId]
|
[newLimit, companyId]
|
||||||
);
|
);
|
||||||
|
if (result.rowCount === 0) {
|
||||||
|
const err = new Error('Company not found: ' + companyId);
|
||||||
|
err.status = 404;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Export ──
|
// ── Export ──
|
||||||
|
|||||||
+2
-1
@@ -54,7 +54,8 @@ function createRouter() {
|
|||||||
// POST /admin/limit/:id — установить лимит компании
|
// POST /admin/limit/:id — установить лимит компании
|
||||||
router.post('/admin/limit/:id', async (req, res) => {
|
router.post('/admin/limit/:id', async (req, res) => {
|
||||||
const token = api.token(req);
|
const token = api.token(req);
|
||||||
const limit = parseInt(req.body.limit, 10);
|
const rawLimit = req.body.limit;
|
||||||
|
const limit = (rawLimit === '' || rawLimit == null) ? null : parseInt(rawLimit, 10);
|
||||||
try {
|
try {
|
||||||
const r = await api.patch('/api/v1/companies/' + req.params.id + '/limit', token, { limit });
|
const r = await api.patch('/api/v1/companies/' + req.params.id + '/limit', token, { limit });
|
||||||
if (r.status === 200) return res.redirect('/admin?success=' + encodeURIComponent('Лимит обновлён'));
|
if (r.status === 200) return res.redirect('/admin?success=' + encodeURIComponent('Лимит обновлён'));
|
||||||
|
|||||||
+7
-1
@@ -47,7 +47,13 @@ function createRouter({ auth, MOCK_USERS, authLimiter }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
req.session.token = token;
|
req.session.token = token;
|
||||||
req.session.user = { clientId: user.clientId, email: user.email, isAdmin: user.isAdmin };
|
req.session.user = {
|
||||||
|
clientId: user.clientId,
|
||||||
|
email: user.email || user.clientId + '@mock.local',
|
||||||
|
companyId: user.companyId || '00000000-0000-0000-0000-000000000001',
|
||||||
|
companyName: user.companyName || user.clientId,
|
||||||
|
isAdmin: user.role === 'admin',
|
||||||
|
};
|
||||||
res.redirect(returnTo || '/');
|
res.redirect(returnTo || '/');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user