feat: double audit — impersonated_by in audit_log

- db.js: add impersonated_by column to audit_log
- queries.js: logAudit accepts impersonatedBy, all CRUD pass it
- api/routes/entries.js: pass req.user.originalUserEmail as impersonatedBy
- При имперсонации: created_by=импактируемый, impersonated_by=админ
This commit is contained in:
2026-06-11 16:10:41 +04:00
parent 9c51e449ab
commit 7758d70dcf
4 changed files with 15 additions and 14 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ipwhitelist", "name": "ipwhitelist",
"version": "0.5.54", "version": "0.5.55",
"description": "IP WhiteList microservice for cloud provider", "description": "IP WhiteList microservice for cloud provider",
"main": "server.js", "main": "server.js",
"scripts": { "scripts": {
+3 -3
View File
@@ -121,7 +121,7 @@ function createEntriesRouter({ q }) {
if (comment && comment.length > 255) return res.status(400).json({ error: 'Комментарий слишком длинный (максимум 255 символов)' }); if (comment && comment.length > 255) return res.status(400).json({ error: 'Комментарий слишком длинный (максимум 255 символов)' });
try { try {
const company = await resolveCompany(req); const company = await resolveCompany(req);
const { entry, wasNormalized } = await q.createEntry(company.id, value, comment || '', req.user.email); const { entry, wasNormalized } = await q.createEntry(company.id, value, comment || '', req.user.email, req.user.originalUserEmail);
res.status(201).json({ entry, wasNormalized }); res.status(201).json({ entry, wasNormalized });
} catch (e) { } catch (e) {
if (e.status) return res.status(e.status).json({ error: e.message }); if (e.status) return res.status(e.status).json({ error: e.message });
@@ -136,7 +136,7 @@ function createEntriesRouter({ q }) {
if (comment && comment.length > 255) return res.status(400).json({ error: 'Комментарий слишком длинный (максимум 255 символов)' }); if (comment && comment.length > 255) return res.status(400).json({ error: 'Комментарий слишком длинный (максимум 255 символов)' });
try { try {
const company = await resolveCompany(req); const company = await resolveCompany(req);
const { entry, wasNormalized } = await q.updateEntry(req.params.id, company.id, value, comment ?? '', req.user.email); const { entry, wasNormalized } = await q.updateEntry(req.params.id, company.id, value, comment ?? '', req.user.email, req.user.originalUserEmail);
res.json({ entry, wasNormalized }); res.json({ entry, wasNormalized });
} catch (e) { } catch (e) {
if (e.status) return res.status(e.status).json({ error: e.message }); if (e.status) return res.status(e.status).json({ error: e.message });
@@ -148,7 +148,7 @@ function createEntriesRouter({ q }) {
router.delete('/:id', async (req, res) => { router.delete('/:id', async (req, res) => {
try { try {
const company = await resolveCompany(req); const company = await resolveCompany(req);
await q.deleteEntry(req.params.id, company.id, req.user.email); await q.deleteEntry(req.params.id, company.id, req.user.email, req.user.originalUserEmail);
res.status(204).send(); res.status(204).send();
} catch (e) { } catch (e) {
if (e.status) return res.status(e.status).json({ error: e.message }); if (e.status) return res.status(e.status).json({ error: e.message });
+1
View File
@@ -56,6 +56,7 @@ async function ensureSchema() {
old_value TEXT, old_value TEXT,
new_value TEXT, new_value TEXT,
entry_id INTEGER, entry_id INTEGER,
impersonated_by VARCHAR(255) DEFAULT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
); );
CREATE INDEX IF NOT EXISTS idx_audit_company_time ON audit_log(company_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_audit_company_time ON audit_log(company_id, created_at DESC);
+10 -10
View File
@@ -30,7 +30,7 @@ async function listEntries(companyId, includeDeleted = false) {
return (await pool.query(sql, [companyId])).rows; return (await pool.query(sql, [companyId])).rows;
} }
async function createEntry(companyId, rawValue, comment, userEmail) { async function createEntry(companyId, rawValue, comment, userEmail, impersonatedBy) {
const { cidr, wasNormalized } = validate(rawValue); const { cidr, wasNormalized } = validate(rawValue);
const client = await pool.connect(); const client = await pool.connect();
try { try {
@@ -64,7 +64,7 @@ async function createEntry(companyId, rawValue, comment, userEmail) {
VALUES ($1, $2, $3, $4) RETURNING *`, VALUES ($1, $2, $3, $4) RETURNING *`,
[companyId, cidr, comment || null, userEmail] [companyId, cidr, comment || null, userEmail]
); );
await logAudit(userEmail, companyId, 'CREATE', null, cidr, res.rows[0].id, client); await logAudit(userEmail, companyId, 'CREATE', null, cidr, res.rows[0].id, impersonatedBy, client);
await client.query('COMMIT'); await client.query('COMMIT');
return { entry: res.rows[0], wasNormalized }; return { entry: res.rows[0], wasNormalized };
} catch (e) { } catch (e) {
@@ -75,7 +75,7 @@ async function createEntry(companyId, rawValue, comment, userEmail) {
} }
} }
async function updateEntry(entryId, companyId, rawValue, comment, userEmail) { async function updateEntry(entryId, companyId, rawValue, comment, userEmail, impersonatedBy) {
const { cidr, wasNormalized } = validate(rawValue); const { cidr, wasNormalized } = validate(rawValue);
const client = await pool.connect(); const client = await pool.connect();
try { try {
@@ -105,7 +105,7 @@ async function updateEntry(entryId, companyId, rawValue, comment, userEmail) {
WHERE id = $4 AND company_id = $5 RETURNING *`, WHERE id = $4 AND company_id = $5 RETURNING *`,
[cidr, comment || old.comment, userEmail, entryId, companyId] [cidr, comment || old.comment, userEmail, entryId, companyId]
); );
await logAudit(userEmail, companyId, 'UPDATE', old.value_cidr, cidr, entryId, client); await logAudit(userEmail, companyId, 'UPDATE', old.value_cidr, cidr, entryId, impersonatedBy, client);
await client.query('COMMIT'); await client.query('COMMIT');
return { entry: res.rows[0], wasNormalized }; return { entry: res.rows[0], wasNormalized };
} catch (e) { } catch (e) {
@@ -116,7 +116,7 @@ async function updateEntry(entryId, companyId, rawValue, comment, userEmail) {
} }
} }
async function deleteEntry(entryId, companyId, userEmail) { async function deleteEntry(entryId, companyId, userEmail, impersonatedBy) {
const client = await pool.connect(); const client = await pool.connect();
try { try {
await client.query('BEGIN'); await client.query('BEGIN');
@@ -132,7 +132,7 @@ async function deleteEntry(entryId, companyId, userEmail) {
'UPDATE whitelist_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] [userEmail, entryId, companyId]
); );
await logAudit(userEmail, companyId, 'DELETE', old.value_cidr, null, entryId, client); await logAudit(userEmail, companyId, 'DELETE', old.value_cidr, null, entryId, impersonatedBy, client);
await client.query('COMMIT'); await client.query('COMMIT');
} catch (e) { } catch (e) {
await client.query('ROLLBACK'); await client.query('ROLLBACK');
@@ -214,11 +214,11 @@ async function getExportCIDRs(companyId = null) {
// ── Audit ── // ── Audit ──
async function logAudit(userEmail, companyId, action, oldValue, newValue, entryId, db = pool) { async function logAudit(userEmail, companyId, action, oldValue, newValue, entryId, impersonatedBy, db = pool) {
await db.query( await db.query(
`INSERT INTO audit_log (user_email, company_id, action, old_value, new_value, entry_id) `INSERT INTO audit_log (user_email, company_id, action, old_value, new_value, entry_id, impersonated_by)
VALUES ($1, $2, $3, $4, $5, $6)`, VALUES ($1, $2, $3, $4, $5, $6, $7)`,
[userEmail, companyId, action, oldValue, newValue, entryId || null] [userEmail, companyId, action, oldValue, newValue, entryId || null, impersonatedBy || null]
); );
} }