diff --git a/src/auth.js b/src/auth.js index 2c9de90..358f3c1 100644 --- a/src/auth.js +++ b/src/auth.js @@ -445,6 +445,22 @@ function createBearerMiddleware(isOidc, devMode) { try { const payload = verifyAnyToken(bearer, devMode); req.user = userFromPayload(payload); + + // Проброс контекста имперсонации из UI-слоя (X-Imp-* заголовки) + // Устанавливаются ui/api-client.js → api.impHeaders() + const impEmail = req.headers['x-imp-email'] || ''; + const impOrig = req.headers['x-imp-originalemail'] || ''; + const impClientId = req.headers['x-imp-clientid'] || ''; + if (impEmail) { + req.user.email = impEmail; + req.user.originalUserEmail = impOrig; + req.user.isImpersonated = true; + if (impClientId) { + req.user.clientId = impClientId; + req.user.activeClientId = impClientId; + } + } + return next(); } catch (e) { return res.status(401).json({ error: 'Invalid token: ' + e.message }); diff --git a/ui/api-client.js b/ui/api-client.js index 9caf734..52f9c0a 100644 --- a/ui/api-client.js +++ b/ui/api-client.js @@ -22,7 +22,7 @@ const API_BASE = process.env.API_BASE || 'http://localhost:' + (process.env.PORT * @param {object} [body] — JSON body (для POST/PATCH) * @returns {Promise<{status: number, data: any}>} */ -function apiRequest(method, path, token, body) { +function apiRequest(method, path, token, body, impHeaders) { return new Promise((resolve, reject) => { const url = new URL(API_BASE + path); const lib = url.protocol === 'https:' ? https : http; @@ -38,6 +38,12 @@ function apiRequest(method, path, token, body) { 'Accept': 'application/json', }, }; + // Проброс контекста имперсонации из UI в API-слой + if (impHeaders) { + if (impHeaders.impEmail) opts.headers['X-Imp-Email'] = impHeaders.impEmail; + if (impHeaders.impOriginalEmail) opts.headers['X-Imp-OriginalEmail'] = impHeaders.impOriginalEmail; + if (impHeaders.impClientId) opts.headers['X-Imp-ClientId'] = impHeaders.impClientId; + } if (bodyStr) { opts.headers['Content-Type'] = 'application/json'; opts.headers['Content-Length'] = Buffer.byteLength(bodyStr); @@ -65,13 +71,23 @@ function apiRequest(method, path, token, body) { // Хелперы — один вызов на метод const api = { - get: (path, token) => apiRequest('GET', path, token), - post: (path, token, body) => apiRequest('POST', path, token, body), - patch: (path, token, body) => apiRequest('PATCH', path, token, body), - delete: (path, token) => apiRequest('DELETE', path, token), + get: (path, token, imp) => apiRequest('GET', path, token, null, imp), + post: (path, token, body, imp) => apiRequest('POST', path, token, body, imp), + patch: (path, token, body, imp) => apiRequest('PATCH', path, token, body, imp), + delete: (path, token, imp) => apiRequest('DELETE', path, token, null, imp), // Достать Bearer из сессии token: (req) => req.session && req.session.token, + + // Построить заголовки имперсонации из req.user (для проброса в API-слой) + impHeaders: (req) => { + if (!req.user || !req.user.isImpersonated) return null; + return { + impEmail: req.user.email, + impOriginalEmail: req.user.originalUserEmail, + impClientId: req.user.activeClientId || req.user.clientId, + }; + }, }; module.exports = api; diff --git a/ui/routes/entries.js b/ui/routes/entries.js index c1fd6c9..b64c1cd 100644 --- a/ui/routes/entries.js +++ b/ui/routes/entries.js @@ -38,6 +38,7 @@ function createRouter() { // GET / — список записей router.get('/', async (req, res) => { const token = api.token(req); + const imp = api.impHeaders(req); // ── Переключатель компании через IAM ──────────────────────────────── const switchTo = (req.query.switchTo || '').trim(); @@ -78,7 +79,7 @@ function createRouter() { let entries = [], limit = 15, selectedCompany = null, companies = []; if (req.user.adminMode) { - const cr = await api.get('/api/v1/companies', token); + const cr = await api.get('/api/v1/companies', token, imp); companies = cr.data.companies || []; const companyId = req.query.company ? parseInt(req.query.company, 10) : null; @@ -91,7 +92,7 @@ function createRouter() { if (selectedCompany) { const includeDeleted = req.query.includeDeleted === 'true' ? '&includeDeleted=true' : ''; - const er = await api.get('/api/v1/entries?company=' + selectedCompany.id + includeDeleted, token); + const er = await api.get('/api/v1/entries?company=' + selectedCompany.id + includeDeleted, token, imp); entries = er.data.entries || []; limit = er.data.limit || 15; } @@ -101,7 +102,7 @@ function createRouter() { const clientIdParam = req.user.allClientIds && req.user.allClientIds.length > 1 ? '?client_id=' + encodeURIComponent(activeClientId) : ''; - const er = await api.get('/api/v1/entries' + clientIdParam, token); + const er = await api.get('/api/v1/entries' + clientIdParam, token, imp); entries = er.data.entries || []; limit = er.data.limit || 15; } @@ -150,11 +151,12 @@ function createRouter() { // POST /add — создать запись router.post('/add', async (req, res) => { const token = api.token(req); + const imp = api.impHeaders(req); const { value, comment } = req.body; const cq = companyQuery(req); const back = backUrl(req); try { - const r = await api.post('/api/v1/entries' + cq, token, { value, comment }); + const r = await api.post('/api/v1/entries' + cq, token, { value, comment }, imp); if (r.status === 201) { const msg = r.data.wasNormalized ? 'Запись добавлена (CIDR нормализован до ' + r.data.entry.value_cidr + ')' @@ -175,11 +177,12 @@ function createRouter() { // POST /edit/:id — обновить запись router.post('/edit/:id', async (req, res) => { const token = api.token(req); + const imp = api.impHeaders(req); const { value, comment } = req.body; const cq = companyQuery(req); const back = backUrl(req); try { - const r = await api.patch('/api/v1/entries/' + req.params.id + cq, token, { value, comment }); + const r = await api.patch('/api/v1/entries/' + req.params.id + cq, token, { value, comment }, imp); if (r.status === 200) { return res.redirect(back + (back.includes('?') ? '&' : '?') + 'success=' + encodeURIComponent('Запись обновлена')); } @@ -197,10 +200,11 @@ function createRouter() { // POST /delete/:id — удалить запись router.post('/delete/:id', async (req, res) => { const token = api.token(req); + const imp = api.impHeaders(req); const cq = companyQuery(req); const back = backUrl(req); try { - const r = await api.delete('/api/v1/entries/' + req.params.id + cq, token); + const r = await api.delete('/api/v1/entries/' + req.params.id + cq, token, imp); if (r.status === 204) { return res.redirect(back + (back.includes('?') ? '&' : '?') + 'success=' + encodeURIComponent('Запись удалена')); } diff --git a/views/audit.ejs b/views/audit.ejs index d81321e..1c20749 100644 --- a/views/audit.ejs +++ b/views/audit.ejs @@ -133,7 +133,7 @@
<%= e.value_cidr %>