getpgdata: JSON API POST /api/sql для произвольных SQL-команд (WRITE_SQL=1), версия 1.0.3
This commit is contained in:
@@ -9,8 +9,36 @@ Node.js приложение для доступа к PostgreSQL (managed k8s, i
|
|||||||
- `GET /` — состояние подключения и список таблиц (не системные схемы)
|
- `GET /` — состояние подключения и список таблиц (не системные схемы)
|
||||||
- `GET /table?schema=public&name=<table>` — просмотр содержимого таблицы с пагинацией
|
- `GET /table?schema=public&name=<table>` — просмотр содержимого таблицы с пагинацией
|
||||||
- `POST /query` — SQL-консоль (read-only, только `SELECT`)
|
- `POST /query` — SQL-консоль (read-only, только `SELECT`)
|
||||||
|
- `POST /api/sql` — JSON API для произвольных SQL-команд (см. ниже)
|
||||||
- `GET /healthz` — liveness-проба для k8s
|
- `GET /healthz` — liveness-проба для k8s
|
||||||
|
|
||||||
|
## JSON API: POST /api/sql
|
||||||
|
|
||||||
|
Выполняет произвольный SQL против БД, на которую настроено приложение (текущие креды).
|
||||||
|
Удобен для переноса данных между БД: читать из прежней (текущие креды) → затем после
|
||||||
|
смены кредов на новую ПГ вставлять.
|
||||||
|
|
||||||
|
**Запрос:**
|
||||||
|
```bash
|
||||||
|
curl -X POST https://<app>/api/sql \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"sql":"SELECT * FROM whitelist_entries LIMIT 10"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Тело:** `{ "sql": "...", "params": [...] }` — `params` подставляются в `$1..$n`.
|
||||||
|
|
||||||
|
**Ответ:**
|
||||||
|
- `SELECT` → `{ "ok": true, "fields": [...], "rows": [...], "rowCount": n }`
|
||||||
|
- `INSERT/UPDATE/DELETE` → `{ "ok": true, "rowCount": n }`
|
||||||
|
- ошибка → HTTP 400/403, `{ "ok": false, "error": "..." }`
|
||||||
|
|
||||||
|
**Ограничения и безопасность:**
|
||||||
|
- Выполняется **один statement** (первый до `;`) — защита от мульти-комманд (`; DROP ...`)
|
||||||
|
- `SELECT`/`WITH` — доступны всегда
|
||||||
|
- `INSERT`/`UPDATE`/`DELETE`/`DDL` — **только при `WRITE_SQL=1`** в env;
|
||||||
|
иначе возвращается HTTP 403 «Write-SQL отключён»
|
||||||
|
- Для включения записи в jsonEnv добавь `"WRITE_SQL": "1"`
|
||||||
|
|
||||||
## Структура
|
## Структура
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -116,3 +116,28 @@ getpgdata/
|
|||||||
**⚠️ Точная причина падения пода по-прежнему не подтверждена** (нет доступа к логам Nubes).
|
**⚠️ Точная причина падения пода по-прежнему не подтверждена** (нет доступа к логам Nubes).
|
||||||
Если под не поднимется и после этого — смотреть логи `nodejsk8s` Deployment в Nubes UI:
|
Если под не поднимется и после этого — смотреть логи `nodejsk8s` Deployment в Nubes UI:
|
||||||
replica не активна (CrashLoopBackOff, invalidImage, порт/health, зависший старт).
|
replica не активна (CrashLoopBackOff, invalidImage, порт/health, зависший старт).
|
||||||
|
|
||||||
|
## 7. JSON API POST /api/sql (перенос данных в новую ПГ)
|
||||||
|
|
||||||
|
**Контекст:** нужно перенести белые списки (IP, кто создал, когда) из прежней БД в **новую**
|
||||||
|
PostgreSQL (тот же realm k8s). Доступ к новой БД — через Keycloak, автоматизировать нельзя.
|
||||||
|
Решение: код в репе `getpgdata` + передеплой с кредами новой ПГ.
|
||||||
|
|
||||||
|
**Что добавлено (версия 1.0.3):**
|
||||||
|
- Эндпоинт **`POST /api/sql`** — JSON API для произвольных SQL-команд.
|
||||||
|
Body: `{ "sql": "...", "params": [...] }` (params → `$1..$n`).
|
||||||
|
- `SELECT`/`WITH` → `{ ok:true, fields, rows, rowCount }` (всегда доступен)
|
||||||
|
- `INSERT`/`UPDATE`/`DELETE`/DDL → `{ ok:true, rowCount }` **только при `WRITE_SQL=1`**
|
||||||
|
- иначе → HTTP 403 «Write-SQL отключён»
|
||||||
|
- **Защита:** выполняется только первый statement (split по `;`) — блокирует мульти-команды
|
||||||
|
вроде `SELECT 1; DROP TABLE ...`.
|
||||||
|
- Флаг в env: `WRITE_SQL=1` включает запись.
|
||||||
|
|
||||||
|
**Логика переноса:**
|
||||||
|
1. Приложение подключено к прежней БД — читать данные через `/api/sql` (SELECT).
|
||||||
|
2. Сменить креды в jsonEnv на новую ПГ (+ `WRITE_SQL=1`).
|
||||||
|
3. Передеплой — теперь `/api/sql` выполняет запросы уже в новой БД; выполнить INSERT данных.
|
||||||
|
|
||||||
|
**Проверено:** `node -c server.js` OK; локально: пустой SQL → 400; SELECT → идёт в БД;
|
||||||
|
`INSERT` без `WRITE_SQL` → 403 (до попытки подключения); `WRITE_SQL=1` + мульти-statement →
|
||||||
|
берётся только первый; процесс проверки остановлен.
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "getpgdata",
|
"name": "getpgdata",
|
||||||
"version": "1.0.2",
|
"version": "1.0.3",
|
||||||
"description": "Node.js приложение для доступа к PostgreSQL (managed k8s, internal-only)",
|
"description": "Node.js приложение для доступа к PostgreSQL (managed k8s, internal-only)",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ app.set('views', path.join(__dirname, 'views'));
|
|||||||
|
|
||||||
const APP_ENV = process.env.APP_ENV || 'production';
|
const APP_ENV = process.env.APP_ENV || 'production';
|
||||||
const QUERY_MAX_ROWS = parseInt(process.env.QUERY_MAX_ROWS || '200', 10);
|
const QUERY_MAX_ROWS = parseInt(process.env.QUERY_MAX_ROWS || '200', 10);
|
||||||
|
// Разрешить произвольный SQL (INSERT/UPDATE/DELETE/DDL) через POST /api/sql.
|
||||||
|
// По умолчанию — только SELECT (read-only). Для включения записи: WRITE_SQL=1.
|
||||||
|
const WRITE_SQL = (process.env.WRITE_SQL || '').toLowerCase() === '1';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Конфигурация подключения к PostgreSQL
|
// Конфигурация подключения к PostgreSQL
|
||||||
@@ -206,6 +209,54 @@ app.post('/query', async (req, res) => {
|
|||||||
res.render('query', { sql, columns, rows, error, elapsed, QUERY_MAX_ROWS });
|
res.render('query', { sql, columns, rows, error, elapsed, QUERY_MAX_ROWS });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// JSON API для произвольных SQL-команд.
|
||||||
|
//
|
||||||
|
// POST /api/sql Body: { "sql": "...", "params": [...] }
|
||||||
|
// SELECT → { ok:true, fields:[...], rows:[...] } (всегда доступен)
|
||||||
|
// INSERT/UPDATE/DELETE/DDL → { ok:true, rowCount:n }
|
||||||
|
// (доступно только при WRITE_SQL=1)
|
||||||
|
//
|
||||||
|
// Ограничение: выполняется ОДИН statement (первый) — предотвращает
|
||||||
|
// многоступенчатые команды через API. Параметры — через $1..$n.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
app.post('/api/sql', async (req, res) => {
|
||||||
|
const sql = (req.body && typeof req.body.sql === 'string' ? req.body.sql : '').trim();
|
||||||
|
const params = Array.isArray(req.body && req.body.params) ? req.body.params : [];
|
||||||
|
|
||||||
|
if (!sql) {
|
||||||
|
return res.status(400).json({ ok: false, error: 'Пустой SQL' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Берём первый statement (до ; и до разделителей) — защита от мульти-комманд
|
||||||
|
const first = sql.split(';')[0].trim();
|
||||||
|
if (!first) {
|
||||||
|
return res.status(400).json({ ok: false, error: 'Пустой SQL' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const isRead = /^(select|with|show|explain)\b/i.test(first);
|
||||||
|
if (!isRead) {
|
||||||
|
if (!WRITE_SQL) {
|
||||||
|
return res.status(403).json({ ok: false, error: 'Write-SQL отключён (нужен WRITE_SQL=1)' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r = await pool.query(first, params);
|
||||||
|
if (r.fields && r.fields.length) {
|
||||||
|
return res.json({
|
||||||
|
ok: true,
|
||||||
|
fields: r.fields.map((f) => f.name),
|
||||||
|
rows: r.rows.slice(0, WRITE_SQL ? 10000 : QUERY_MAX_ROWS),
|
||||||
|
rowCount: r.rowCount,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return res.json({ ok: true, rowCount: r.rowCount == null ? 0 : r.rowCount });
|
||||||
|
} catch (e) {
|
||||||
|
return res.status(400).json({ ok: false, error: e.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Обработка 404
|
// Обработка 404
|
||||||
app.use((req, res) => {
|
app.use((req, res) => {
|
||||||
res.status(404).render('error', { code: 404, message: 'Страница не найдена' });
|
res.status(404).render('error', { code: 404, message: 'Страница не найдена' });
|
||||||
|
|||||||
Reference in New Issue
Block a user