Export: убран filename из Content-Disposition для диалога сохранения
Версия: 0.1.13 Документация: db-query-endpoint.md
This commit is contained in:
@@ -6,6 +6,7 @@ __pycache__/
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
|
*.har
|
||||||
|
|
||||||
# Креды, токены, конфиги
|
# Креды, токены, конфиги
|
||||||
token*.*
|
token*.*
|
||||||
|
|||||||
@@ -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-инструмента)
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ipwhitelist",
|
"name": "ipwhitelist",
|
||||||
"version": "0.1.12",
|
"version": "0.1.13",
|
||||||
"description": "IP WhiteList microservice for cloud provider",
|
"description": "IP WhiteList microservice for cloud provider",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
version: '0.1.12',
|
version: '0.1.13',
|
||||||
|
|
||||||
// ── Среда ─────────────────────────────────────────────────────────────────
|
// ── Среда ─────────────────────────────────────────────────────────────────
|
||||||
// APP_ENV=test → тестовый стенд (жёлтый баннер в UI, можно включать тестовый API)
|
// APP_ENV=test → тестовый стенд (жёлтый баннер в UI, можно включать тестовый API)
|
||||||
|
|||||||
@@ -45,11 +45,10 @@ function createExportRouter() {
|
|||||||
return res.send(text);
|
return res.send(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Скачивание (по умолчанию)
|
// Скачивание (по умолчанию) — без filename чтобы браузер показал диалог сохранения
|
||||||
const text = aggregated.join('\n') + (aggregated.length ? '\n' : '');
|
const text = aggregated.join('\n') + (aggregated.length ? '\n' : '');
|
||||||
res.set('Content-Type', 'text/plain; charset=utf-8');
|
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');
|
||||||
res.set('Content-Disposition', 'attachment; filename="' + fname + '"');
|
|
||||||
res.send(text);
|
res.send(text);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[v2:export] Error:', e);
|
console.error('[v2:export] Error:', e);
|
||||||
|
|||||||
Reference in New Issue
Block a user