feat: full IAM impersonation support — read-only + original user + canWrite
- 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
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ipwhitelist",
|
"name": "ipwhitelist",
|
||||||
"version": "0.5.52",
|
"version": "0.5.53",
|
||||||
"description": "IP WhiteList microservice for cloud provider",
|
"description": "IP WhiteList microservice for cloud provider",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+9
-1
@@ -312,6 +312,8 @@ async function fetchIamUser(token) {
|
|||||||
const profiles = raw.profiles || [];
|
const profiles = raw.profiles || [];
|
||||||
const activeProfile = profiles.find(p => p.is_active_profile) || profiles[0] || {};
|
const activeProfile = profiles.find(p => p.is_active_profile) || profiles[0] || {};
|
||||||
const ui = raw.userInfo || {};
|
const ui = raw.userInfo || {};
|
||||||
|
const imp = raw.impersonation || {};
|
||||||
|
const perms = raw.permissions || {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
email: ui.email || '',
|
email: ui.email || '',
|
||||||
@@ -321,7 +323,13 @@ 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),
|
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,
|
fio: ui.fio || null,
|
||||||
profiles, // полный массив для UI
|
profiles, // полный массив для UI
|
||||||
raw, // сырой ответ (для отладки)
|
raw, // сырой ответ (для отладки)
|
||||||
|
|||||||
@@ -73,7 +73,12 @@ function createRouter({ auth, doubleCsrfProtection, generateCsrfToken, authLimit
|
|||||||
companyId: iamData.companyId,
|
companyId: iamData.companyId,
|
||||||
companyName: iamData.companyName,
|
companyName: iamData.companyName,
|
||||||
isAdmin: iamData.isAdmin,
|
isAdmin: iamData.isAdmin,
|
||||||
|
canWrite: iamData.canWrite,
|
||||||
isImpersonated: iamData.isImpersonated,
|
isImpersonated: iamData.isImpersonated,
|
||||||
|
impersonationType: iamData.impersonationType,
|
||||||
|
originalUserEmail: iamData.originalUserEmail,
|
||||||
|
originalUserFullName: iamData.originalUserFullName,
|
||||||
|
originalUserCompany: iamData.originalUserCompany,
|
||||||
fio: iamData.fio,
|
fio: iamData.fio,
|
||||||
profiles: iamData.profiles,
|
profiles: iamData.profiles,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ function createUiRouter({ auth, MOCK_USERS, authLimiter }) {
|
|||||||
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;
|
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 = {
|
req.user = {
|
||||||
clientId: rawClientId,
|
clientId: rawClientId,
|
||||||
allClientIds,
|
allClientIds,
|
||||||
@@ -72,7 +73,10 @@ 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,
|
||||||
|
canWrite: sessionCanWrite,
|
||||||
isImpersonated: !!sessionIsImpersonated,
|
isImpersonated: !!sessionIsImpersonated,
|
||||||
|
originalUserEmail: req.session?.user?.originalUserEmail || '',
|
||||||
|
originalUserFullName: req.session?.user?.originalUserFullName || '',
|
||||||
canAdminMode,
|
canAdminMode,
|
||||||
adminMode: !!(req.session && req.session.adminMode),
|
adminMode: !!(req.session && req.session.adminMode),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -120,6 +120,8 @@ function createRouter() {
|
|||||||
selectedCompany,
|
selectedCompany,
|
||||||
canAdminMode: req.user.canAdminMode,
|
canAdminMode: req.user.canAdminMode,
|
||||||
isImpersonated: req.user.isImpersonated,
|
isImpersonated: req.user.isImpersonated,
|
||||||
|
originalUserEmail: req.user.originalUserEmail,
|
||||||
|
canWrite: req.user.canWrite,
|
||||||
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 переключателя
|
||||||
@@ -136,6 +138,8 @@ function createRouter() {
|
|||||||
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,
|
isImpersonated: req.user.isImpersonated,
|
||||||
|
originalUserEmail: req.user.originalUserEmail,
|
||||||
|
canWrite: req.user.canWrite,
|
||||||
companies: [], selectedCompany: null,
|
companies: [], selectedCompany: null,
|
||||||
allClientIds: null, activeClientId: null,
|
allClientIds: null, activeClientId: null,
|
||||||
error: 'Ошибка загрузки: ' + e.message,
|
error: 'Ошибка загрузки: ' + e.message,
|
||||||
@@ -147,6 +151,7 @@ function createRouter() {
|
|||||||
|
|
||||||
// POST /add — создать запись
|
// POST /add — создать запись
|
||||||
router.post('/add', async (req, res) => {
|
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 token = api.token(req);
|
||||||
const { value, comment } = req.body;
|
const { value, comment } = req.body;
|
||||||
const cq = companyQuery(req);
|
const cq = companyQuery(req);
|
||||||
@@ -172,6 +177,7 @@ function createRouter() {
|
|||||||
|
|
||||||
// POST /edit/:id — обновить запись
|
// POST /edit/:id — обновить запись
|
||||||
router.post('/edit/:id', async (req, res) => {
|
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 token = api.token(req);
|
||||||
const { value, comment } = req.body;
|
const { value, comment } = req.body;
|
||||||
const cq = companyQuery(req);
|
const cq = companyQuery(req);
|
||||||
@@ -194,6 +200,7 @@ function createRouter() {
|
|||||||
|
|
||||||
// POST /delete/:id — удалить запись
|
// POST /delete/:id — удалить запись
|
||||||
router.post('/delete/:id', async (req, res) => {
|
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 token = api.token(req);
|
||||||
const cq = companyQuery(req);
|
const cq = companyQuery(req);
|
||||||
const back = backUrl(req);
|
const back = backUrl(req);
|
||||||
|
|||||||
@@ -185,6 +185,14 @@
|
|||||||
<% if (typeof isImpersonated !== 'undefined' && isImpersonated) { %>
|
<% 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;">
|
<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>
|
⚠️ Режим имперсонации — данные сохраняются от имени <strong><%= user.clientId %></strong>
|
||||||
|
<% if (typeof originalUserEmail !== 'undefined' && originalUserEmail) { %>
|
||||||
|
(вы: <%= originalUserEmail %>)
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
<% if (typeof canWrite !== 'undefined' && !canWrite) { %>
|
||||||
|
<div style="background:#fee2e2;border:1px solid #dc2626;color:#991b1b;padding:.5rem 1rem;border-radius:8px;margin-bottom:.5rem;font-size:.85rem;">
|
||||||
|
🔒 Режим только для чтения — редактирование запрещено
|
||||||
</div>
|
</div>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
@@ -279,6 +287,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Форма добавления -->
|
<!-- Форма добавления -->
|
||||||
|
<% if (typeof canWrite === 'undefined' || canWrite) { %>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">Добавить адрес</div>
|
<div class="card-header">Добавить адрес</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -308,6 +317,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
<!-- Таблица -->
|
<!-- Таблица -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@@ -341,6 +351,7 @@
|
|||||||
: '—' %>
|
: '—' %>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center" style="white-space:nowrap;">
|
<td class="text-center" style="white-space:nowrap;">
|
||||||
|
<% if (typeof canWrite === 'undefined' || canWrite) { %>
|
||||||
<%
|
<%
|
||||||
// data-* атрибуты для JS-заполнения modal без inline-onclick с параметрами.
|
// data-* атрибуты для JS-заполнения modal без inline-onclick с параметрами.
|
||||||
// Так как комментарий может содержать кавычки — EJS экранирует их в HTML.
|
// Так как комментарий может содержать кавычки — EJS экранирует их в HTML.
|
||||||
@@ -365,6 +376,7 @@
|
|||||||
<input type="hidden" name="_csrf" value="<%= csrfToken %>">
|
<input type="hidden" name="_csrf" value="<%= csrfToken %>">
|
||||||
<button class="btn btn-danger">Удалить</button>
|
<button class="btn btn-danger">Удалить</button>
|
||||||
</form>
|
</form>
|
||||||
|
<% } %>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
|
|||||||
Reference in New Issue
Block a user