From 4bdcb4c363ff8f50fb95dc30a2b228856cac9039 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Wed, 8 Jul 2026 14:57:17 +0400 Subject: [PATCH] =?UTF-8?q?Export:=20=D1=83=D0=B1=D1=80=D0=B0=D0=BD=20file?= =?UTF-8?q?name=20=D0=B8=D0=B7=20Content-Disposition=20=D0=B4=D0=BB=D1=8F?= =?UTF-8?q?=20=D0=B4=D0=B8=D0=B0=D0=BB=D0=BE=D0=B3=D0=B0=20=D1=81=D0=BE?= =?UTF-8?q?=D1=85=D1=80=D0=B0=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=92=D0=B5?= =?UTF-8?q?=D1=80=D1=81=D0=B8=D1=8F:=200.1.13=20=D0=94=D0=BE=D0=BA=D1=83?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=86=D0=B8=D1=8F:=20db-query-en?= =?UTF-8?q?dpoint.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + docs/db-query-endpoint.md | 76 +++++++++++++++++++++++++++++++++++++++ package.json | 2 +- v2/src/config/index.js | 2 +- v2/src/export/index.js | 5 ++- 5 files changed, 81 insertions(+), 5 deletions(-) create mode 100644 docs/db-query-endpoint.md diff --git a/.gitignore b/.gitignore index ade7633..dc350c1 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ __pycache__/ .DS_Store .vscode/ .idea/ +*.har # Креды, токены, конфиги token*.* diff --git a/docs/db-query-endpoint.md b/docs/db-query-endpoint.md new file mode 100644 index 0000000..049f0fe --- /dev/null +++ b/docs/db-query-endpoint.md @@ -0,0 +1,76 @@ +# Endpoint SQL-запросов к БД (`/v2/db/query`) + +> Создан: 2026-07-08 + +## Назначение + +Дать DevOps-инженеру (одному человеку, заданному через `ADMIN_EMAIL`) возможность выполнять SQL-запросы к БД напрямую из браузера — без psql, без kubectl, без внешнего IP. + +## Доступ + +- Только один пользователь: `req.email === ADMIN_EMAIL` +- Обычный юзер сервиса (проходит SSO как все) +- Пример: `ADMIN_EMAIL=atazet@narod.ru` + +## Адрес + +``` +GET /v2/db/query?sql=<часть запроса после SELECT> +``` + +## Как работает + +1. Пользователь заходит в сервис через SSO (любой URL, например `/v2/app`) +2. Меняет в адресной строке путь на `/v2/db/query?sql=...` +3. Сервер: + - Проверяет сессию (resolveContext) + - Проверяет что `req.email === ADMIN_EMAIL` + - Добавляет префикс `SELECT` перед вводом пользователя + - Выполняет: `pool.query('SELECT ' + userInput)` + - Возвращает JSON + +## Примеры + +| Ввод в браузере | Что выполняется | +|---|---| +| `/v2/db/query?sql=* FROM _migrations` | `SELECT * FROM _migrations` | +| `/v2/db/query?sql=count(*) FROM companies` | `SELECT count(*) FROM companies` | +| `/v2/db/query?sql=* FROM whitelist_entries LIMIT 10` | `SELECT * FROM whitelist_entries LIMIT 10` | +| `/v2/db/query?sql=* FROM audit_log ORDER BY created_at DESC LIMIT 5` | `SELECT * FROM audit_log ORDER BY created_at DESC LIMIT 5` | + +## Безопасность + +1. **SELECT префикс** — жёстко добавляется `SELECT` перед вводом пользователя. DROP/INSERT/UPDATE/DELETE не работают: + ``` + /v2/db/query?sql=DROP TABLE companies + → SELECT DROP TABLE companies (ошибка SQL, не выполнится) + ``` + +2. **Одиночные запросы** — драйвер `pg` не поддерживает множественные запросы в одном `query()`. Попытка `1; DROP TABLE` не сработает. + +3. **Доступ по email** — только один человек, заданный в `ADMIN_EMAIL`. + +4. **Rate limiter** — endpoint защищён лимитером (как mutationLimiter). + +## Ответ + +```json +{ + "rows": [ ... ], + "rowCount": 5, + "fields": ["id", "filename", "applied_at"] +} +``` + +## Реализация + +- Файл: `v2/src/db/query.js` +- Middleware: `resolveContext` + проверка `req.canQueryDB` +- Роутер монтируется в `v2/server.js` на `/db` +- Флаг `req.canQueryDB` выставляется в `v2/src/router/index.js` (applyContext) + +## Ограничения + +- Только SELECT (через префикс) +- Нет GUI — только JSON в браузере +- SQL в URL (логируется в access_log nginx/ingress — это нормально для devops-инструмента) diff --git a/package.json b/package.json index db1ef0b..358049e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ipwhitelist", - "version": "0.1.12", + "version": "0.1.13", "description": "IP WhiteList microservice for cloud provider", "main": "server.js", "scripts": { diff --git a/v2/src/config/index.js b/v2/src/config/index.js index e35b90a..8da03aa 100644 --- a/v2/src/config/index.js +++ b/v2/src/config/index.js @@ -8,7 +8,7 @@ // ═══════════════════════════════════════════════════════════════════════════════ module.exports = { - version: '0.1.12', + version: '0.1.13', // ── Среда ───────────────────────────────────────────────────────────────── // APP_ENV=test → тестовый стенд (жёлтый баннер в UI, можно включать тестовый API) diff --git a/v2/src/export/index.js b/v2/src/export/index.js index ecbfeb7..5ee9315 100644 --- a/v2/src/export/index.js +++ b/v2/src/export/index.js @@ -45,11 +45,10 @@ function createExportRouter() { return res.send(text); } - // Скачивание (по умолчанию) + // Скачивание (по умолчанию) — без filename чтобы браузер показал диалог сохранения const text = aggregated.join('\n') + (aggregated.length ? '\n' : ''); res.set('Content-Type', 'text/plain; charset=utf-8'); - const fname = (req.query.filename || 'white-list.txt').replace(/[^\w\-_. ]/g, '_'); - res.set('Content-Disposition', 'attachment; filename="' + fname + '"'); + res.set('Content-Disposition', 'attachment'); res.send(text); } catch (e) { console.error('[v2:export] Error:', e);