From 9a331eec820fd9c19e9976dd79a2f8467664fd26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Thu, 11 Jun 2026 15:54:56 +0400 Subject: [PATCH] =?UTF-8?q?feat:=20full=20IAM=20impersonation=20support=20?= =?UTF-8?q?=E2=80=94=20read-only=20+=20original=20user=20+=20canWrite?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/auth.js: all impersonation fields + permissions.can_write - src/routes/oidc.js: save all fields to session - ui/index.js: canWrite, originalUserEmail in req.user - ui/routes/entries.js: read-only guard on POST/PATCH/DELETE - docs/PLAN-impersonation.md: updated with implemented items --- package.json | 2 +- src/auth.js | 10 +++++++++- src/routes/oidc.js | 5 +++++ ui/index.js | 4 ++++ ui/routes/entries.js | 7 +++++++ views/index.ejs | 12 ++++++++++++ 6 files changed, 38 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 9d69948..e4fd02c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ipwhitelist", - "version": "0.5.52", + "version": "0.5.53", "description": "IP WhiteList microservice for cloud provider", "main": "server.js", "scripts": { diff --git a/src/auth.js b/src/auth.js index b41cc98..ef62de6 100644 --- a/src/auth.js +++ b/src/auth.js @@ -312,6 +312,8 @@ async function fetchIamUser(token) { const profiles = raw.profiles || []; const activeProfile = profiles.find(p => p.is_active_profile) || profiles[0] || {}; const ui = raw.userInfo || {}; + const imp = raw.impersonation || {}; + const perms = raw.permissions || {}; return { email: ui.email || '', @@ -321,7 +323,13 @@ async function fetchIamUser(token) { companyId: ui.companyId || '', companyName: ui.company || activeProfile.company_name || '', isAdmin: !!ui.isAdmin, - isImpersonated: !!(raw.impersonation && raw.impersonation.is_impersonated), + canWrite: perms.can_write !== false, + isImpersonated: !!(imp.is_impersonated), + impersonationType: imp.type || null, + originalUserEmail: imp.originalUserEmail || '', + originalUserFullName: imp.originalUserFullName || '', + originalUserCompany: imp.originalUserCompany || '', + impersonatedCompanyId: imp.impersonatedCompanyId || '', fio: ui.fio || null, profiles, // полный массив для UI raw, // сырой ответ (для отладки) diff --git a/src/routes/oidc.js b/src/routes/oidc.js index da8b97a..4577b92 100644 --- a/src/routes/oidc.js +++ b/src/routes/oidc.js @@ -73,7 +73,12 @@ function createRouter({ auth, doubleCsrfProtection, generateCsrfToken, authLimit companyId: iamData.companyId, companyName: iamData.companyName, isAdmin: iamData.isAdmin, + canWrite: iamData.canWrite, isImpersonated: iamData.isImpersonated, + impersonationType: iamData.impersonationType, + originalUserEmail: iamData.originalUserEmail, + originalUserFullName: iamData.originalUserFullName, + originalUserCompany: iamData.originalUserCompany, fio: iamData.fio, profiles: iamData.profiles, }; diff --git a/ui/index.js b/ui/index.js index 9043cf5..9380eb3 100644 --- a/ui/index.js +++ b/ui/index.js @@ -64,6 +64,7 @@ function createUiRouter({ auth, MOCK_USERS, authLimiter }) { const canAdminMode = !!(iamAdmin && activeClientId === ADMIN_CLIENT_ID) || (isNail && activeClientId === ADMIN_CLIENT_ID); const sessionIsImpersonated = req.session && req.session.user && req.session.user.isImpersonated; + const sessionCanWrite = req.session && req.session.user ? req.session.user.canWrite : true; req.user = { clientId: rawClientId, allClientIds, @@ -72,7 +73,10 @@ function createUiRouter({ auth, MOCK_USERS, authLimiter }) { companyName: payload.company_name || payload.companyName || activeClientId, email: req.session?.user?.email || payload.email || payload.login || activeClientId + '@unknown', isAdmin: iamAdmin || isNail, + canWrite: sessionCanWrite, isImpersonated: !!sessionIsImpersonated, + originalUserEmail: req.session?.user?.originalUserEmail || '', + originalUserFullName: req.session?.user?.originalUserFullName || '', canAdminMode, adminMode: !!(req.session && req.session.adminMode), }; diff --git a/ui/routes/entries.js b/ui/routes/entries.js index c3e4c34..838b10b 100644 --- a/ui/routes/entries.js +++ b/ui/routes/entries.js @@ -120,6 +120,8 @@ function createRouter() { selectedCompany, canAdminMode: req.user.canAdminMode, isImpersonated: req.user.isImpersonated, + originalUserEmail: req.user.originalUserEmail, + canWrite: req.user.canWrite, allClientIds: hasMultiple ? req.user.allClientIds : null, activeClientId: req.user.activeClientId || req.user.clientId, profiles, // из IAM — для UI переключателя @@ -136,6 +138,8 @@ function createRouter() { user: req.user, isAdmin: req.user.isAdmin, adminMode: req.user.adminMode, canAdminMode: req.user.canAdminMode, isImpersonated: req.user.isImpersonated, + originalUserEmail: req.user.originalUserEmail, + canWrite: req.user.canWrite, companies: [], selectedCompany: null, allClientIds: null, activeClientId: null, error: 'Ошибка загрузки: ' + e.message, @@ -147,6 +151,7 @@ function createRouter() { // POST /add — создать запись router.post('/add', async (req, res) => { + if (req.user && !req.user.canWrite) return res.status(403).send('Read-only mode'); const token = api.token(req); const { value, comment } = req.body; const cq = companyQuery(req); @@ -172,6 +177,7 @@ function createRouter() { // POST /edit/:id — обновить запись router.post('/edit/:id', async (req, res) => { + if (req.user && !req.user.canWrite) return res.status(403).send('Read-only mode'); const token = api.token(req); const { value, comment } = req.body; const cq = companyQuery(req); @@ -194,6 +200,7 @@ function createRouter() { // POST /delete/:id — удалить запись router.post('/delete/:id', async (req, res) => { + if (req.user && !req.user.canWrite) return res.status(403).send('Read-only mode'); const token = api.token(req); const cq = companyQuery(req); const back = backUrl(req); diff --git a/views/index.ejs b/views/index.ejs index 37fdbdb..ef687c2 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -185,6 +185,14 @@ <% if (typeof isImpersonated !== 'undefined' && isImpersonated) { %>
⚠️ Режим имперсонации — данные сохраняются от имени <%= user.clientId %> + <% if (typeof originalUserEmail !== 'undefined' && originalUserEmail) { %> + (вы: <%= originalUserEmail %>) + <% } %> +
+ <% } %> + <% if (typeof canWrite !== 'undefined' && !canWrite) { %> +
+ 🔒 Режим только для чтения — редактирование запрещено
<% } %> @@ -279,6 +287,7 @@ + <% if (typeof canWrite === 'undefined' || canWrite) { %>
Добавить адрес
@@ -308,6 +317,7 @@
+ <% } %>
@@ -341,6 +351,7 @@ : '—' %> + <% if (typeof canWrite === 'undefined' || canWrite) { %> <% // data-* атрибуты для JS-заполнения modal без inline-onclick с параметрами. // Так как комментарий может содержать кавычки — EJS экранирует их в HTML. @@ -365,6 +376,7 @@ + <% } %> <% }) %>