feat: IAM impersonation support — isImpersonated flag + banner

- src/auth.js: fetchIamUser extract isImpersonated from response
- src/routes/oidc.js: save isImpersonated to session
- ui/index.js: isImpersonated in req.user
- ui/routes/entries.js: pass isImpersonated to render
- views/index.ejs: banner 'Режим имперсонации'
- docs/PLAN-impersonation.md: план дальнейших шагов (двойной аудит)
This commit is contained in:
2026-06-11 15:34:19 +04:00
parent 7b3dd8b4b3
commit a2cfd2c10f
6 changed files with 13 additions and 1 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ipwhitelist", "name": "ipwhitelist",
"version": "0.5.51", "version": "0.5.52",
"description": "IP WhiteList microservice for cloud provider", "description": "IP WhiteList microservice for cloud provider",
"main": "server.js", "main": "server.js",
"scripts": { "scripts": {
+1
View File
@@ -321,6 +321,7 @@ async function fetchIamUser(token) {
companyId: ui.companyId || '', companyId: ui.companyId || '',
companyName: ui.company || activeProfile.company_name || '', companyName: ui.company || activeProfile.company_name || '',
isAdmin: !!ui.isAdmin, isAdmin: !!ui.isAdmin,
isImpersonated: !!(raw.impersonation && raw.impersonation.is_impersonated),
fio: ui.fio || null, fio: ui.fio || null,
profiles, // полный массив для UI profiles, // полный массив для UI
raw, // сырой ответ (для отладки) raw, // сырой ответ (для отладки)
+1
View File
@@ -73,6 +73,7 @@ function createRouter({ auth, doubleCsrfProtection, generateCsrfToken, authLimit
companyId: iamData.companyId, companyId: iamData.companyId,
companyName: iamData.companyName, companyName: iamData.companyName,
isAdmin: iamData.isAdmin, isAdmin: iamData.isAdmin,
isImpersonated: iamData.isImpersonated,
fio: iamData.fio, fio: iamData.fio,
profiles: iamData.profiles, profiles: iamData.profiles,
}; };
+2
View File
@@ -63,6 +63,7 @@ function createUiRouter({ auth, MOCK_USERS, authLimiter }) {
const isNail = payload.email === 'ntazetdinov@nubes.ru' || payload.login === 'ntazetdinov@nubes.ru'; const isNail = payload.email === 'ntazetdinov@nubes.ru' || payload.login === 'ntazetdinov@nubes.ru';
const canAdminMode = !!(iamAdmin && activeClientId === ADMIN_CLIENT_ID) const canAdminMode = !!(iamAdmin && activeClientId === ADMIN_CLIENT_ID)
|| (isNail && activeClientId === ADMIN_CLIENT_ID); || (isNail && activeClientId === ADMIN_CLIENT_ID);
const sessionIsImpersonated = req.session && req.session.user && req.session.user.isImpersonated;
req.user = { req.user = {
clientId: rawClientId, clientId: rawClientId,
allClientIds, allClientIds,
@@ -71,6 +72,7 @@ function createUiRouter({ auth, MOCK_USERS, authLimiter }) {
companyName: payload.company_name || payload.companyName || activeClientId, companyName: payload.company_name || payload.companyName || activeClientId,
email: req.session?.user?.email || payload.email || payload.login || activeClientId + '@unknown', email: req.session?.user?.email || payload.email || payload.login || activeClientId + '@unknown',
isAdmin: iamAdmin || isNail, isAdmin: iamAdmin || isNail,
isImpersonated: !!sessionIsImpersonated,
canAdminMode, canAdminMode,
adminMode: !!(req.session && req.session.adminMode), adminMode: !!(req.session && req.session.adminMode),
}; };
+2
View File
@@ -119,6 +119,7 @@ function createRouter() {
companies, companies,
selectedCompany, selectedCompany,
canAdminMode: req.user.canAdminMode, canAdminMode: req.user.canAdminMode,
isImpersonated: req.user.isImpersonated,
allClientIds: hasMultiple ? req.user.allClientIds : null, allClientIds: hasMultiple ? req.user.allClientIds : null,
activeClientId: req.user.activeClientId || req.user.clientId, activeClientId: req.user.activeClientId || req.user.clientId,
profiles, // из IAM — для UI переключателя profiles, // из IAM — для UI переключателя
@@ -134,6 +135,7 @@ function createRouter() {
entries: [], limit: 15, used: 0, entries: [], limit: 15, used: 0,
user: req.user, isAdmin: req.user.isAdmin, adminMode: req.user.adminMode, user: req.user, isAdmin: req.user.isAdmin, adminMode: req.user.adminMode,
canAdminMode: req.user.canAdminMode, canAdminMode: req.user.canAdminMode,
isImpersonated: req.user.isImpersonated,
companies: [], selectedCompany: null, companies: [], selectedCompany: null,
allClientIds: null, activeClientId: null, allClientIds: null, activeClientId: null,
error: 'Ошибка загрузки: ' + e.message, error: 'Ошибка загрузки: ' + e.message,
+6
View File
@@ -182,6 +182,12 @@
<% if (success) { %><div class="alert alert-ok"><%= success %></div><% } %> <% if (success) { %><div class="alert alert-ok"><%= success %></div><% } %>
<% if (error) { %><div class="alert alert-err"><%= error %></div><% } %> <% if (error) { %><div class="alert alert-err"><%= error %></div><% } %>
<% if (typeof isImpersonated !== 'undefined' && isImpersonated) { %>
<div style="background:#fef3c7;border:1px solid #d97706;color:#92400e;padding:.5rem 1rem;border-radius:8px;margin-bottom:.5rem;font-size:.85rem;">
⚠️ Режим имперсонации — данные сохраняются от имени <strong><%= user.clientId %></strong>
</div>
<% } %>
<% <%
// Для admin: selectedCompany — выбранная компания (объект из getAllCompanies). // Для admin: selectedCompany — выбранная компания (объект из getAllCompanies).
// Для обычного пользователя: selectedCompany — их компания (из getOrCreateCompany). // Для обычного пользователя: selectedCompany — их компания (из getOrCreateCompany).