From 814d066b10809d0a6d443fd33ff7430f8e125e4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Mon, 15 Jun 2026 09:16:10 +0400 Subject: [PATCH] =?UTF-8?q?v2:=20=D1=82=D0=B0=D0=B1=D0=BB=D0=B8=D1=86?= =?UTF-8?q?=D1=8B=20v2=5F=E2=86=92=D0=BE=D1=80=D0=B8=D0=B3=D0=B8=D0=BD?= =?UTF-8?q?=D0=B0=D0=BB=D1=8C=D0=BD=D1=8B=D0=B5=20+=20impersonation=20?= =?UTF-8?q?=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8C=20(IMPERSONATION=5FTARGET?= =?UTF-8?q?=20+=20EXTRA=5FCOMPANIES)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- v2/server.js | 5 ++-- v2/src/config/index.js | 2 +- v2/src/db/queries.js | 48 +++++++++++++++++------------------ v2/src/db/schema.js | 18 ++++++------- v2/src/impersonation/index.js | 32 +++++++++++++++++++++++ 6 files changed, 70 insertions(+), 37 deletions(-) create mode 100644 v2/src/impersonation/index.js diff --git a/package.json b/package.json index d5699d2..ccba39c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ipwhitelist", - "version": "0.5.97", + "version": "0.5.98", "description": "IP WhiteList microservice for cloud provider", "main": "server.js", "scripts": { diff --git a/v2/server.js b/v2/server.js index 77d9e64..9e95a79 100644 --- a/v2/server.js +++ b/v2/server.js @@ -26,6 +26,7 @@ const express = require('express'); const config = require('./src/config'); const auth = require('./src/auth'); const { resolveContext } = require('./src/router'); +const { enhanceImpersonation } = require('./src/impersonation'); const { createUserRouter } = require('./src/user'); const { createAdminRouter } = require('./src/admin'); const { pool } = require('./src/db'); @@ -50,10 +51,10 @@ function createV2Router() { }); // ── /v2/app — пользовательский CRUD (только с сессией) ────────────── - router.use('/app', resolveContext, createUserRouter()); + router.use('/app', enhanceImpersonation, resolveContext, createUserRouter()); // ── /v2/admin — админка (только для isAdmin=true) ──────────────────── - router.use('/admin', resolveContext, createAdminRouter()); + router.use('/admin', enhanceImpersonation, resolveContext, createAdminRouter()); // ── GET /v2/ — редирект на /v2/app ────────────────────────────────── router.get('/', (req, res) => res.redirect('/v2/app')); diff --git a/v2/src/config/index.js b/v2/src/config/index.js index 8d3e552..4559e21 100644 --- a/v2/src/config/index.js +++ b/v2/src/config/index.js @@ -9,7 +9,7 @@ // ═══════════════════════════════════════════════════════════════════════════════ module.exports = { - version: '0.5.97', + version: '0.5.98', // ── IAM ────────────────────────────────────────────────────────────────── iamUrl: process.env.V2_IAM_URL || 'https://auth-api.ngcloud.ru/api/v1/auth/user', diff --git a/v2/src/db/queries.js b/v2/src/db/queries.js index 01cebff..bcde74a 100644 --- a/v2/src/db/queries.js +++ b/v2/src/db/queries.js @@ -10,14 +10,14 @@ // 3. Фронтенды не видят SQL — только через crud API. // // ЧТО ПРИНИМАЕТ: -// companyId — внутренний id из v2_companies (число). +// companyId — внутренний id из companies (число). // cidr — УЖЕ проверенный CIDR (валидация в crud/). // email, impBy — для аудита. // // overlaps() — проверка пересечений с существующими записями (только здесь, // потому что требует данных из БД). // -// ТАБЛИЦЫ: v2_companies, v2_entries, v2_audit (префикс v2_ — изоляция). +// ТАБЛИЦЫ: companies, whitelist_entries, audit_log (префикс v2_ — изоляция). // ═══════════════════════════════════════════════════════════════════════════════ const { pool } = require('./index'); @@ -31,8 +31,8 @@ const { overlaps } = require('../validators'); */ async function getOrCreateCompany(clientId, companyName) { const res = await pool.query( - `INSERT INTO v2_companies (client_id, name) VALUES ($1, $2) - ON CONFLICT (client_id) DO UPDATE SET name = COALESCE(v2_companies.name, EXCLUDED.name) + `INSERT INTO companies (client_id, name) VALUES ($1, $2) + ON CONFLICT (client_id) DO UPDATE SET name = COALESCE(companies.name, EXCLUDED.name) RETURNING *`, [clientId, companyName || clientId] ); @@ -51,11 +51,11 @@ async function getLimit(company) { /** * Список записей компании. - * @param {number} companyId — v2_companies.id + * @param {number} companyId — companies.id * @param {boolean} includeDeleted — показать удалённые */ async function listEntries(companyId, includeDeleted = false) { - let sql = 'SELECT * FROM v2_entries WHERE company_id = $1'; + let sql = 'SELECT * FROM whitelist_entries WHERE company_id = $1'; const params = [companyId]; if (!includeDeleted) sql += ' AND deleted_at IS NULL'; sql += ' ORDER BY created_at DESC'; @@ -71,19 +71,19 @@ async function createEntry(companyId, cidr, comment, userEmail, impersonatedBy, await client.query('BEGIN'); const company = (await client.query( - 'SELECT * FROM v2_companies WHERE id = $1 FOR UPDATE', [companyId] + 'SELECT * FROM companies WHERE id = $1 FOR UPDATE', [companyId] )).rows[0]; if (!company) throw new Error('Компания не найдена'); const limit = await getLimit(company); const cnt = (await client.query( - 'SELECT COUNT(*)::int AS c FROM v2_entries WHERE company_id = $1 AND deleted_at IS NULL', + 'SELECT COUNT(*)::int AS c FROM whitelist_entries WHERE company_id = $1 AND deleted_at IS NULL', [companyId] )).rows[0].c; if (cnt >= limit) throw new Error(`Лимит исчерпан: ${cnt} из ${limit}`); const existing = (await client.query( - 'SELECT value_cidr FROM v2_entries WHERE company_id = $1 AND deleted_at IS NULL', + 'SELECT value_cidr FROM whitelist_entries WHERE company_id = $1 AND deleted_at IS NULL', [companyId] )).rows; for (const row of existing) { @@ -93,7 +93,7 @@ async function createEntry(companyId, cidr, comment, userEmail, impersonatedBy, } const res = await client.query( - `INSERT INTO v2_entries (company_id, value_cidr, comment, created_by) + `INSERT INTO whitelist_entries (company_id, value_cidr, comment, created_by) VALUES ($1, $2, $3, $4) RETURNING *`, [companyId, cidr, comment || null, userEmail] ); @@ -115,16 +115,16 @@ async function updateEntry(entryId, companyId, cidr, comment, userEmail, imperso const client = await pool.connect(); try { await client.query('BEGIN'); - await client.query('SELECT * FROM v2_companies WHERE id = $1 FOR UPDATE', [companyId]); + await client.query('SELECT * FROM companies WHERE id = $1 FOR UPDATE', [companyId]); const old = (await client.query( - 'SELECT * FROM v2_entries WHERE id = $1 AND company_id = $2 AND deleted_at IS NULL', + 'SELECT * FROM whitelist_entries WHERE id = $1 AND company_id = $2 AND deleted_at IS NULL', [entryId, companyId] )).rows[0]; if (!old) throw new Error('Запись не найдена'); const existing = (await client.query( - 'SELECT value_cidr FROM v2_entries WHERE company_id = $1 AND deleted_at IS NULL AND id <> $2', + 'SELECT value_cidr FROM whitelist_entries WHERE company_id = $1 AND deleted_at IS NULL AND id <> $2', [companyId, entryId] )).rows; for (const row of existing) { @@ -134,7 +134,7 @@ async function updateEntry(entryId, companyId, cidr, comment, userEmail, imperso } const res = await client.query( - `UPDATE v2_entries SET value_cidr = $1, comment = $2, updated_by = $3, updated_at = NOW() + `UPDATE whitelist_entries SET value_cidr = $1, comment = $2, updated_by = $3, updated_at = NOW() WHERE id = $4 AND company_id = $5 RETURNING *`, [cidr, comment || old.comment, userEmail, entryId, companyId] ); @@ -158,13 +158,13 @@ async function deleteEntry(entryId, companyId, userEmail, impersonatedBy) { await client.query('BEGIN'); const old = (await client.query( - 'SELECT * FROM v2_entries WHERE id = $1 AND company_id = $2 AND deleted_at IS NULL', + 'SELECT * FROM whitelist_entries WHERE id = $1 AND company_id = $2 AND deleted_at IS NULL', [entryId, companyId] )).rows[0]; if (!old) throw new Error('Запись не найдена'); await client.query( - 'UPDATE v2_entries SET deleted_by = $1, deleted_at = NOW() WHERE id = $2 AND company_id = $3', + 'UPDATE whitelist_entries SET deleted_by = $1, deleted_at = NOW() WHERE id = $2 AND company_id = $3', [userEmail, entryId, companyId] ); await logAudit(userEmail, companyId, 'DELETE', old.value_cidr, null, entryId, impersonatedBy, client); @@ -180,15 +180,15 @@ async function deleteEntry(entryId, companyId, userEmail, impersonatedBy) { // ── Админ ───────────────────────────────────────────────────────────────────── async function getCompanyById(id) { - const res = await pool.query('SELECT * FROM v2_companies WHERE id = $1', [id]); + const res = await pool.query('SELECT * FROM companies WHERE id = $1', [id]); return res.rows[0] || null; } async function getAllCompanies() { const res = await pool.query(` SELECT c.*, COUNT(e.id)::int AS active_count - FROM v2_companies c - LEFT JOIN v2_entries e ON e.company_id = c.id AND e.deleted_at IS NULL + FROM companies c + LEFT JOIN whitelist_entries e ON e.company_id = c.id AND e.deleted_at IS NULL GROUP BY c.id ORDER BY c.name, c.client_id `); @@ -197,7 +197,7 @@ async function getAllCompanies() { async function setLimit(companyId, newLimit) { const result = await pool.query( - 'UPDATE v2_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] ); if (result.rowCount === 0) { @@ -210,7 +210,7 @@ async function setLimit(companyId, newLimit) { // ── Экспорт ─────────────────────────────────────────────────────────────────── async function getExportCIDRs(companyId = null) { - let sql = 'SELECT value_cidr FROM v2_entries WHERE deleted_at IS NULL'; + let sql = 'SELECT value_cidr FROM whitelist_entries WHERE deleted_at IS NULL'; const params = []; if (companyId) { sql += ' AND company_id = $1'; params.push(companyId); } sql += ' ORDER BY value_cidr'; @@ -221,7 +221,7 @@ async function getExportCIDRs(companyId = null) { async function logAudit(userEmail, companyId, action, oldValue, newValue, entryId, impersonatedBy, db = pool) { await db.query( - `INSERT INTO v2_audit (user_email, company_id, action, old_value, new_value, entry_id, impersonated_by) + `INSERT INTO audit_log (user_email, company_id, action, old_value, new_value, entry_id, impersonated_by) VALUES ($1, $2, $3, $4, $5, $6, $7)`, [userEmail, companyId, action, oldValue, newValue, entryId || null, impersonatedBy || null] ); @@ -230,8 +230,8 @@ async function logAudit(userEmail, companyId, action, oldValue, newValue, entryI async function getAudit(companyId = null) { let sql = ` SELECT a.*, c.name AS company_name, c.client_id - FROM v2_audit a - JOIN v2_companies c ON c.id = a.company_id + FROM audit_log a + JOIN companies c ON c.id = a.company_id `; const params = []; if (companyId) { sql += ' WHERE a.company_id = $1'; params.push(companyId); } diff --git a/v2/src/db/schema.js b/v2/src/db/schema.js index ca67b55..1afed66 100644 --- a/v2/src/db/schema.js +++ b/v2/src/db/schema.js @@ -8,9 +8,9 @@ // 3. Безопасно для production: IF NOT EXISTS. // // ТАБЛИЦЫ: -// v2_companies — client_id (W-номер), name, custom_limit -// v2_entries — CIDR, комментарий, кто создал/изменил/удалил -// v2_audit — все действия: CREATE/UPDATE/DELETE, с impersonated_by +// companies — client_id (W-номер), name, custom_limit +// whitelist_entries — CIDR, комментарий, кто создал/изменил/удалил +// audit_log — все действия: CREATE/UPDATE/DELETE, с impersonated_by // // ИНДЕКСЫ: на company_id для быстрых SELECT + на unique(client_id). // ═══════════════════════════════════════════════════════════════════════════════ @@ -18,7 +18,7 @@ async function ensureSchema(pool) { await pool.query(` -- Компании: W-номер уникален, upsert через ON CONFLICT - CREATE TABLE IF NOT EXISTS v2_companies ( + CREATE TABLE IF NOT EXISTS companies ( id SERIAL PRIMARY KEY, client_id VARCHAR(64) NOT NULL UNIQUE, name VARCHAR(255), @@ -28,7 +28,7 @@ async function ensureSchema(pool) { ); -- Записи: CIDR + метаданные, soft delete - CREATE TABLE IF NOT EXISTS v2_entries ( + CREATE TABLE IF NOT EXISTS whitelist_entries ( id SERIAL PRIMARY KEY, company_id INTEGER NOT NULL, value_cidr VARCHAR(64) NOT NULL, @@ -42,7 +42,7 @@ async function ensureSchema(pool) { ); -- Аудит: каждое действие с возможностью отследить имперсонацию - CREATE TABLE IF NOT EXISTS v2_audit ( + CREATE TABLE IF NOT EXISTS audit_log ( id SERIAL PRIMARY KEY, user_email VARCHAR(255) NOT NULL, company_id INTEGER NOT NULL, @@ -54,11 +54,11 @@ async function ensureSchema(pool) { created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); - CREATE INDEX IF NOT EXISTS idx_v2_audit_company_time - ON v2_audit(company_id, created_at DESC); + CREATE INDEX IF NOT EXISTS idx_audit_log_company_time + ON audit_log(company_id, created_at DESC); `); - console.log('[v2:db] Schema ensured'); + console.log('[db] Schema ensured'); } module.exports = { ensureSchema }; diff --git a/v2/src/impersonation/index.js b/v2/src/impersonation/index.js new file mode 100644 index 0000000..c7e5366 --- /dev/null +++ b/v2/src/impersonation/index.js @@ -0,0 +1,32 @@ +// ═══════════════════════════════════════════════════════════════════════════════ +// V2 — Расширение имперсонации (отдельный модуль) +// +// ЭТО: middleware — добавляет дополнительные компании имперсонируемому юзеру. +// НЕ: жёсткая логика в resolveContext или user/index.js. +// +// ENV: +// IMPERSONATION_TARGET — email юзера, которому добавляем компании +// IMPERSONATION_EXTRA_COMPANIES — W-номера через запятую (WZ01112,WZ03709) +// ═══════════════════════════════════════════════════════════════════════════════ + +const TARGET_EMAIL = process.env.IMPERSONATION_TARGET || ''; +const EXTRA_COMPANIES = (process.env.IMPERSONATION_EXTRA_COMPANIES || '') + .split(',').map(s => s.trim()).filter(Boolean); + +function enhanceImpersonation(req, res, next) { + if (!TARGET_EMAIL) return next(); + const u = req.session && req.session.user; + if (!u || u.email !== TARGET_EMAIL) return next(); + + const current = u.allClientIds || [u.clientId || '']; + u.allClientIds = [...new Set([...current, ...EXTRA_COMPANIES])].filter(Boolean); + + if (!u.profiles) u.profiles = []; + for (const cid of EXTRA_COMPANIES) { + if (!u.profiles.find(p => p.client_id === cid)) + u.profiles.push({ client_id: cid, company_name: cid, is_active_profile: false }); + } + next(); +} + +module.exports = { enhanceImpersonation, TARGET_EMAIL, EXTRA_COMPANIES };