Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e6e22fadc | ||
|
|
7049b5aaa9 | ||
|
|
f709959478 | ||
|
|
d2c621cf38 | ||
|
|
400908985e | ||
|
|
2d164fb9ee | ||
|
|
c823ae8128 | ||
|
|
b1d670a6bf | ||
|
|
d72bb84d25 | ||
|
|
fb17c83e60 | ||
|
|
f60713f127 | ||
|
|
74f805682f | ||
|
|
060b20002c | ||
|
|
24fb8cdc56 | ||
|
|
710dcfc7c9 | ||
|
|
c50ef814e2 | ||
|
|
30001062d4 | ||
|
|
92f0314a20 | ||
|
|
fa75f8317d | ||
|
|
90b6c509c0 |
+15
@@ -0,0 +1,15 @@
|
||||
Доверенные адреса – защита вашего трафика во время DDoS-атак
|
||||
Когда на инфраструктуру провайдера идёт DDoS-атака, системы защиты автоматически фильтруют подозрительный трафик. Иногда под фильтрацию попадает и легитимный трафик, например, запросы из вашего офиса или от ваших партнёров.
|
||||
Чтобы этого не происходило, вы можете заранее указать адреса, которые провайдер гарантированно не заблокирует. Это называется списком доверенных адресов.
|
||||
Как пользоваться
|
||||
<тут можно вставить ссылку на сервис>
|
||||
Зайти в сервис необходимо с учетной записью keycloak.
|
||||
Добавить адрес. Нажмите «Добавить», введите IPv4-адрес или подсеть в формате CIDR и при желании — комментарий, чтобы не забыть, что это за адрес. Например: 203.0.113.10 или 203.0.113.0/24. Нажмите «Сохранить». Если вы вводите адрес хоста с маской подсети (например 203.0.113.5/24 вместо 203.0.113.0/24), сервис автоматически нормализует его до адреса подсети и сообщит вам об этом.
|
||||
Добавляйте комментарии к записям — через несколько месяцев вы скажете себе спасибо: будет сразу понятно, чей это адрес и зачем он добавлен.
|
||||
Редактировать или удалить. Рядом с каждой записью есть кнопки редактирования и удаления. Удалённые записи перестают действовать через 5 минут.
|
||||
Ограничения
|
||||
• Только публичные IPv4-адреса. Серые адреса (192.168.x.x, 10.x.x.x, 172.16–31.x.x и другие служебные диапазоны) добавить нельзя — они не маршрутизируются в интернете и смысла в их добавлении нет.
|
||||
• Только IPv4. IPv6-адреса и доменные имена не поддерживаются.
|
||||
• Максимальный размер подсети — /22. Добавить префикс шире /22 (то есть /21, /20 и т.д.) нельзя.
|
||||
• До 15 записей на компанию. Если вам нужно больше — обратитесь в поддержку.
|
||||
• Нет дублей и пересечений. Если вы попытаетесь добавить адрес, который уже покрывается существующей записью (или наоборот), сервис предупредит об этом.
|
||||
@@ -0,0 +1,65 @@
|
||||
# IAM — переменные окружения
|
||||
|
||||
> Дата: 2026-06-17
|
||||
> Путь: `src/config.js`, `v2/src/config/index.js`
|
||||
|
||||
## IAM_API_URL (основная, приоритет выше)
|
||||
|
||||
**Где используется:** `src/config.js` → `src/auth.js` (fetchIamUser, switchProfile)
|
||||
|
||||
**Формат:** `https://auth-api.ngcloud.ru` (без /api/v1)
|
||||
|
||||
Код добавляет `/api/v1/auth/user` автоматически.
|
||||
|
||||
**Назначение:** получение профилей пользователя, переключение компании.
|
||||
|
||||
**Дефолт:** `https://auth-api.ngcloud.ru`
|
||||
|
||||
**Пример:**
|
||||
- IAM_API_URL=https://auth-api.ngcloud.ru
|
||||
- Фактический путь: https://auth-api.ngcloud.ru/api/v1/auth/user
|
||||
|
||||
## IAM_API_BASE (запасная, приоритет ниже)
|
||||
|
||||
**Где используется:** `v2/src/config/index.js` → `v2/server.js` (тестовые страницы)
|
||||
|
||||
**Формат:** `https://lk-api-gateway.ngcloud.ru/api/v1/iam`
|
||||
|
||||
Код НЕ добавляет /api/v1, путь должен быть полный.
|
||||
|
||||
**Назначение:** тестовые страницы (/v2/iam-gateway-test), заглушка пока IAM не починят.
|
||||
|
||||
**Дефолт:** берётся из IAM_API_URL
|
||||
|
||||
**Пример:**
|
||||
- IAM_API_BASE=https://lk-api-gateway.ngcloud.ru/api/v1/iam
|
||||
- Фактический путь: https://lk-api-gateway.ngcloud.ru/api/v1/iam/auth/user
|
||||
|
||||
## Приоритет в src/config.js
|
||||
|
||||
```
|
||||
process.env.IAM_API_URL || process.env.IAM_API_BASE || 'https://auth-api.ngcloud.ru'
|
||||
```
|
||||
|
||||
1. IAM_API_URL — явно задан (auth-api)
|
||||
2. IAM_API_BASE — запасной (lk-api-gateway)
|
||||
3. Дефолт — auth-api.ngcloud.ru
|
||||
|
||||
## Когда что использовать
|
||||
|
||||
| Ситуация | Какая переменная | Какой API |
|
||||
|----------|-----------------|-----------|
|
||||
| Обычная работа (логин, CRUD, компании) | IAM_API_URL | auth-api.ngcloud.ru |
|
||||
| Имперсонация (админ работает от имени юзера) | IAM_API_BASE | lk-api-gateway.ngcloud.ru |
|
||||
| Когда IAM починят | IAM_API_URL | auth-api.ngcloud.ru (везде) |
|
||||
|
||||
## Текущий статус
|
||||
|
||||
- auth-api.ngcloud.ru — не отдаёт profiles таргета при имперсонации (баг разработчиков IAM)
|
||||
- lk-api-gateway.ngcloud.ru — работает, отдаёт profiles таргета, НО не принимает токен от Keycloak whitelist (только от ЛК)
|
||||
- Когда IAM починят: удалить IAM_API_BASE из кода и Nubes UI, оставить только IAM_API_URL
|
||||
|
||||
## Nubes UI (текущие переменные)
|
||||
|
||||
- IAM_API_URL=https://auth-api.ngcloud.ru
|
||||
- IAM_API_BASE=https://lk-api-gateway.ngcloud.ru/api/v1/iam
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ipwhitelist",
|
||||
"version": "0.5.147",
|
||||
"version": "0.5.163",
|
||||
"description": "IP WhiteList microservice for cloud provider",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
|
||||
+4
-3
@@ -25,7 +25,8 @@ const jwt = require('jsonwebtoken');
|
||||
const crypto = require('crypto');
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
const { IAM_API_URL } = require('./config');
|
||||
const { iamApiBase } = require('../v2/src/config');
|
||||
const IAM_API_URL = iamApiBase;
|
||||
|
||||
// ── Конфигурация из окружения ────────────────────────────────────────────────
|
||||
const ISSUER = process.env.JWT_ISSUER || 'mock-auth-api';
|
||||
@@ -286,7 +287,7 @@ function verifyAnyToken(token, devMode) {
|
||||
*/
|
||||
async function fetchIamUser(token) {
|
||||
const raw = await new Promise((resolve, reject) => {
|
||||
const url = new URL(IAM_API_URL + '/api/v1/auth/user');
|
||||
const url = new URL(IAM_API_URL + '/auth/user');
|
||||
const lib = url.protocol === 'https:' ? https : http;
|
||||
const req = lib.request({
|
||||
hostname: url.hostname,
|
||||
@@ -343,7 +344,7 @@ async function fetchIamUser(token) {
|
||||
async function switchProfile(token, profileId) {
|
||||
const body = JSON.stringify({ profile_id: profileId });
|
||||
const raw = await new Promise((resolve, reject) => {
|
||||
const url = new URL(IAM_API_URL + '/api/v1/user/switch-profile');
|
||||
const url = new URL(IAM_API_URL + '/user/switch-profile');
|
||||
const lib = url.protocol === 'https:' ? https : http;
|
||||
const req = lib.request({
|
||||
hostname: url.hostname,
|
||||
|
||||
+2
-2
@@ -13,7 +13,7 @@
|
||||
// ── IAM API ───────────────────────────────────────────────────────────────────
|
||||
// URL IAM-сервиса для получения профилей пользователя (GET /api/v1/auth/user)
|
||||
// и переключения активной компании (POST /api/v1/user/switch-profile).
|
||||
const IAM_API_URL = (process.env.IAM_API_URL || 'https://auth-api-dev.ngcloud.ru').replace(/\/$/, '');
|
||||
const IAM_API_URL = (process.env.IAM_API_URL || process.env.IAM_API_BASE || 'https://auth-api.ngcloud.ru').replace(/\/$/, '');
|
||||
|
||||
// ── Мок-пользователи (только dev/staging) ─────────────────────────────────────
|
||||
// В продакшене этот список не используется — вход через Keycloak.
|
||||
@@ -94,4 +94,4 @@ function backUrl(isAdmin, companyId, extra = {}) {
|
||||
// Передаётся в шаблоны через app.locals чтобы отображаться в UI.
|
||||
const { version: APP_VERSION } = require('../package.json');
|
||||
|
||||
module.exports = { MOCK_USERS, backUrl, APP_VERSION, IAM_API_URL };
|
||||
module.exports = { MOCK_USERS, backUrl, APP_VERSION, IAM_API_URL: (process.env.IAM_API_URL || process.env.IAM_API_BASE || 'https://auth-api.ngcloud.ru').replace(/\/$/, '') };
|
||||
|
||||
@@ -21,7 +21,7 @@ const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const IAM_API_URL = (process.env.IAM_API_URL || 'https://auth-api.ngcloud.ru').replace(/\/$/, '');
|
||||
const IAM_API_URL = (process.env.IAM_API_BASE || 'https://auth-api.ngcloud.ru').replace(/\/$/, '');
|
||||
const APP_URL = (process.env.APP_URL || 'https://italo.kube5s.ru').replace(/\/$/, '');
|
||||
|
||||
// ── Парсинг файла с токенами ────────────────────────────────────────────────
|
||||
|
||||
+66
-21
@@ -46,14 +46,14 @@ function createV2Router() {
|
||||
// ── GET /v2/iam — отладка: показать IAM-данные из сессии ────────────
|
||||
router.get('/iam', (req, res) => {
|
||||
const u = req.session && req.session.user;
|
||||
if (!u) return res.send('<h2>Нет данных</h2><p><a href="/v2/login">Войти</a></p>');
|
||||
if (!u) return res.redirect('/v2/login');
|
||||
res.send(debugPage(u, config.version));
|
||||
});
|
||||
|
||||
// ── GET /v2/iam_response — сырой ответ IAM ─────────────────────────
|
||||
router.get('/iam_response', async (req, res) => {
|
||||
const token = req.session && req.session.token;
|
||||
if (!token) return res.send('<h2>Нет токена</h2><p><a href="/v2/login">Войти</a></p>');
|
||||
if (!token) return res.redirect('/v2/login');
|
||||
try {
|
||||
const mainAuth = require('../src/auth');
|
||||
const iamData = await mainAuth.fetchIamUser(token);
|
||||
@@ -67,9 +67,9 @@ function createV2Router() {
|
||||
// ── GET /v2/iam_response_test — IAM test стенд ──────────────────────
|
||||
router.get('/iam_response_test', async (req, res) => {
|
||||
const token = req.session && req.session.token;
|
||||
if (!token) return res.send('<h2>Нет токена</h2><p><a href="/v2/login">Войти</a></p>');
|
||||
if (!token) return res.redirect('/v2/login');
|
||||
try {
|
||||
const raw = await iamFetch(token, 'https://auth-api-test.ngcloud.ru/api/v1/auth/user');
|
||||
const raw = await iamFetch(token, config.iamApiBase + '/auth/user');
|
||||
res.set('Content-Type', 'text/html; charset=utf-8');
|
||||
res.send(rawPage('IAM TEST', raw));
|
||||
} catch (e) {
|
||||
@@ -80,9 +80,9 @@ function createV2Router() {
|
||||
// ── GET /v2/iam_response_dev — IAM dev стенд ────────────────────────
|
||||
router.get('/iam_response_dev', async (req, res) => {
|
||||
const token = req.session && req.session.token;
|
||||
if (!token) return res.send('<h2>Нет токена</h2><p><a href="/v2/login">Войти</a></p>');
|
||||
if (!token) return res.redirect('/v2/login');
|
||||
try {
|
||||
const raw = await iamFetch(token, 'https://auth-api-dev.ngcloud.ru/api/v1/auth/user');
|
||||
const raw = await iamFetch(token, config.iamApiBase + '/auth/user');
|
||||
res.set('Content-Type', 'text/html; charset=utf-8');
|
||||
res.send(rawPage('IAM DEV', raw));
|
||||
} catch (e) {
|
||||
@@ -90,29 +90,39 @@ function createV2Router() {
|
||||
}
|
||||
});
|
||||
|
||||
// ── GET /v2/impersonation-check — IAM impersonation: 3 GET эндпоинта ──
|
||||
router.get('/impersonation-check', async (req, res) => {
|
||||
// ── GET /v2/iam-gateway-test — сравнение auth-api vs lk-api-gateway ──
|
||||
router.get('/iam-gateway-test', async (req, res) => {
|
||||
const token = req.session && req.session.token;
|
||||
if (!token) return res.send('<h2>Нет токена</h2><p><a href="/v2/login">Войти</a></p>');
|
||||
if (!token) return res.redirect('/v2/login');
|
||||
|
||||
const endpoints = [
|
||||
{ name: 'status', url: 'https://auth-api.ngcloud.ru/api/v1/impersonation/status' },
|
||||
{ name: 'history', url: 'https://auth-api.ngcloud.ru/api/v1/impersonation/history' },
|
||||
{ name: 'history/all', url: 'https://auth-api.ngcloud.ru/api/v1/impersonation/history/all' },
|
||||
const base = config.iamApiBase;
|
||||
const tests = [
|
||||
{
|
||||
name: 'auth-api /auth/user',
|
||||
url: base + '/auth/user',
|
||||
},
|
||||
{
|
||||
name: 'lk-api-gateway /iam/auth/user',
|
||||
url: 'https://lk-api-gateway.ngcloud.ru/api/v1/iam/auth/user',
|
||||
},
|
||||
{
|
||||
name: 'auth-api /impersonation/status',
|
||||
url: base + '/impersonation/status',
|
||||
},
|
||||
];
|
||||
|
||||
const results = {};
|
||||
for (const ep of endpoints) {
|
||||
for (const t of tests) {
|
||||
try {
|
||||
const data = await iamFetch(token, ep.url);
|
||||
results[ep.name] = { ok: true, data };
|
||||
const data = await iamFetch(token, t.url);
|
||||
results[t.name] = { ok: true, data };
|
||||
} catch (e) {
|
||||
results[ep.name] = { ok: false, error: e.message };
|
||||
results[t.name] = { ok: false, error: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
res.set('Content-Type', 'text/html; charset=utf-8');
|
||||
res.send(iamStatPage(results));
|
||||
res.send(iamComparePage(results));
|
||||
});
|
||||
|
||||
// ── /v2/app — пользовательский CRUD (только с сессией) ──────────────
|
||||
@@ -135,8 +145,43 @@ function createV2Router() {
|
||||
const { createExportRouter } = require('./src/export');
|
||||
router.use('/export', createExportRouter());
|
||||
|
||||
// ── GET /v2/logout — редирект на основной выход ─────────────────────
|
||||
router.get('/logout', (req, res) => res.redirect('/logout'));
|
||||
// ── GET /v2/logout — выход только из whitelist (без KC SSO) ─────────
|
||||
router.get('/logout', (req, res) => {
|
||||
const ua = (req.headers['user-agent'] || '').toLowerCase();
|
||||
const isBrowser = ua.includes('mozilla') || ua.includes('chrome') || ua.includes('safari');
|
||||
if (!isBrowser) {
|
||||
req.session.destroy(() => res.redirect('/v2/login'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.query.confirm === 'yes') {
|
||||
req.session.destroy(() => {
|
||||
res.set('Content-Type', 'text/html; charset=utf-8');
|
||||
res.send('<!DOCTYPE html><html><head><meta charset="utf-8"><title>Выход</title>'
|
||||
+ '<style>body{font-family:sans-serif;background:#111;color:#eee;padding:60px;text-align:center}'
|
||||
+ 'h2{color:#4fc3f7}p{font-size:18px;margin:20px 0}a{color:#80cbc4;font-size:16px}'
|
||||
+ '</style></head><body>'
|
||||
+ '<h2>Вы вышли из сервиса</h2>'
|
||||
+ '<p>Сессия whitelist завершена.<br>Сессия в других сервисах Nubes сохранена.</p>'
|
||||
+ '<a href="/v2/app">Войти снова</a>'
|
||||
+ '</body></html>');
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.set('Content-Type', 'text/html; charset=utf-8');
|
||||
res.send('<!DOCTYPE html><html><head><meta charset="utf-8"><title>Выход</title>'
|
||||
+ '<style>body{font-family:sans-serif;background:#111;color:#eee;padding:40px;text-align:center}'
|
||||
+ 'h2{color:#4fc3f7}p{font-size:16px;margin:20px 0}'
|
||||
+ '.btn{display:inline-block;padding:12px 24px;margin:0 10px;border-radius:6px;text-decoration:none;font-size:16px}'
|
||||
+ '.btn-yes{background:#ef5350;color:#fff}.btn-no{background:#333;color:#eee}'
|
||||
+ '</style></head><body>'
|
||||
+ '<h2>Выход из сервиса</h2>'
|
||||
+ '<p>Вы будете выведены только из whitelist.<br>Сессия в других сервисах Nubes сохранится.</p>'
|
||||
+ '<a href="/v2/logout?confirm=yes" class="btn btn-yes">Выйти</a>'
|
||||
+ '<a href="/v2/app" class="btn btn-no">Отмена</a>'
|
||||
+ '</body></html>');
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
@@ -187,7 +232,7 @@ function debugPage(u, version) {
|
||||
</body></html>`;
|
||||
}
|
||||
|
||||
function iamStatPage(results) {
|
||||
function iamComparePage(results) {
|
||||
function fmt(data, depth) {
|
||||
if (data === null || data === undefined) return '<span style="color:#888">null</span>';
|
||||
if (typeof data === 'string') return '<span style="color:#ce93d8">"' + data.replace(/</g, '<') + '"</span>';
|
||||
|
||||
@@ -4,18 +4,18 @@
|
||||
// ЭТО: все настройки в одном месте.
|
||||
// ЗАЧЕМ: всё что может поменяться между средами — здесь, не размазано по коду.
|
||||
//
|
||||
// ПРИОРИТЕТ: process.env (V2_*) → хардкод-умолчания.
|
||||
// Умолчания — для тестовой среды (VM KC на italo.kube5s.ru:8080).
|
||||
// ПРИОРИТЕТ: process.env → хардкод-умолчания.
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
module.exports = {
|
||||
version: '0.5.146',
|
||||
version: '0.5.163',
|
||||
|
||||
// ── IAM ──────────────────────────────────────────────────────────────────
|
||||
iamUrl: process.env.V2_IAM_URL || 'https://auth-api.ngcloud.ru/api/v1/auth/user',
|
||||
iamApiBase: process.env.IAM_API_BASE || 'https://auth-api.ngcloud.ru/api/v1',
|
||||
|
||||
// ── OIDC (Keycloak) ──────────────────────────────────────────────────────
|
||||
kcBaseUrl: process.env.V2_KC_BASE_URL || 'https://italo.kube5s.ru:8080/realms/ipwhitelist',
|
||||
kcBaseUrl: process.env.V2_KC_BASE_URL || 'https://login.ngcloud.ru/realms/ipwhitelist',
|
||||
kcClientId: process.env.V2_KC_CLIENT_ID || 'ipwhitelist-app',
|
||||
kcClientSecret: process.env.V2_KC_CLIENT_SECRET || '',
|
||||
appUrl: process.env.V2_APP_URL || 'https://whitelist.nodejsk8s.services.ngcloud.ru',
|
||||
|
||||
+94
-37
@@ -1,49 +1,108 @@
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// V2 — resolveContext middleware
|
||||
//
|
||||
// ЭТО: Express middleware — извлекает контекст из сессии.
|
||||
// НЕ: БД, бизнес-логика.
|
||||
//
|
||||
// ЗАЧЕМ:
|
||||
// 1. Единственное место где сессия превращается в req.* поля.
|
||||
// 2. user/ и admin/ не парсят сессию сами — получают готовые req.email, req.clientId.
|
||||
// 3. Поддержка имперсонации (админ действует от имени компании).
|
||||
//
|
||||
// ПОРЯДОК:
|
||||
// 1. Нет сессии → 302 /v2/login
|
||||
// 2. Обычный юзер → req.email = session.user.email, req.clientId = activeClientId
|
||||
// 3. Админ → req.isAdmin = true (если adminMode в сессии)
|
||||
// 4. Имперсонация → req.email = originalUserEmail, req.impersonatedBy = админ
|
||||
//
|
||||
// ВЫХОД (поля на req):
|
||||
// req.email — почта (при имперсонации — originalUserEmail)
|
||||
// req.clientId — W-номер текущей компании
|
||||
// req.companyName — название компании
|
||||
// req.isAdmin — флаг админа
|
||||
// req.isImpersonated — флаг имперсонации
|
||||
// req.impersonatedBy — почта админа (null если нет имперсонации)
|
||||
// req.allClientIds — все W-номера юзера
|
||||
// req.profiles — профили из IAM
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
const https = require('https');
|
||||
const config = require('../config');
|
||||
|
||||
function resolveContext(req, res, next) {
|
||||
/**
|
||||
* Express middleware — извлекает контекст из сессии.
|
||||
* При 401 от IAM — чистит сессию и редиректит на логин (SSO).
|
||||
*/
|
||||
async function resolveContext(req, res, next) {
|
||||
const u = req.session && req.session.user;
|
||||
|
||||
// Нет сессии — нет доступа
|
||||
if (!u) return res.redirect('/v2/login');
|
||||
|
||||
// Имперсонация: админ действует от имени компании
|
||||
// u.originalUserEmail — реальный админ
|
||||
// u.impersonatedCompanyId — W-номер компании
|
||||
const token = req.session && req.session.token;
|
||||
if (token) {
|
||||
try {
|
||||
await checkImpersonation(token, req, u);
|
||||
} catch (e) {
|
||||
const msg = (e.message || '');
|
||||
// 401 — токен протух, чистим сессию → SSO даст свежий
|
||||
if (msg.startsWith('401')) {
|
||||
req.session.destroy(() => res.redirect('/v2/login'));
|
||||
return;
|
||||
}
|
||||
// Другие ошибки IAM — игнорируем, используем старые данные
|
||||
}
|
||||
}
|
||||
|
||||
applyContext(req, u);
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Запрашивает IAM и обновляет сессию если имперсонация изменилась.
|
||||
* Бросает ошибку при 401 — вызывающий обработает.
|
||||
*/
|
||||
async function checkImpersonation(token, req, u) {
|
||||
const url = config.iamApiBase + '/auth/user';
|
||||
const uObj = new URL(url);
|
||||
const data = await new Promise((resolve, reject) => {
|
||||
https.get({
|
||||
hostname: uObj.hostname, port: 443, path: uObj.pathname,
|
||||
headers: { Authorization: 'Bearer ' + token, Accept: 'application/json' },
|
||||
}, res => {
|
||||
let d = '';
|
||||
res.on('data', c => d += c);
|
||||
res.on('end', () => {
|
||||
if (res.statusCode !== 200) return reject(new Error(res.statusCode + ': ' + d.slice(0, 200)));
|
||||
try { resolve(JSON.parse(d)); } catch (e) { reject(e); }
|
||||
});
|
||||
}).on('error', reject);
|
||||
});
|
||||
|
||||
const imp = data.impersonation || {};
|
||||
const newIsImp = !!(imp.is_impersonated);
|
||||
const oldIsImp = !!(u.originalUserEmail && u.impersonatedCompanyId);
|
||||
|
||||
// Статус не изменился — ничего не делаем
|
||||
if (newIsImp === oldIsImp) return;
|
||||
|
||||
if (newIsImp) {
|
||||
// Имперсонация началась — обновляем сессию данными таргета
|
||||
const profiles = data.profiles || [];
|
||||
const activeProfile = profiles.find(p => p.is_active_profile) || profiles[0] || {};
|
||||
const ui = data.userInfo || {};
|
||||
|
||||
req.session.user = {
|
||||
...u,
|
||||
email: ui.email || '',
|
||||
clientId: ui.clientID || activeProfile.client_id || '',
|
||||
allClientIds: profiles.map(p => p.client_id).filter(Boolean),
|
||||
activeClientId: ui.clientID || activeProfile.client_id || '',
|
||||
activeProfileId: activeProfile.id || null,
|
||||
companyId: ui.companyId || '',
|
||||
companyName: ui.company || activeProfile.company_name || '',
|
||||
isAdmin: !!ui.isAdmin,
|
||||
isImpersonated: true,
|
||||
impersonationType: imp.type || null,
|
||||
impersonatedCompanyId: imp.impersonatedCompanyId || '',
|
||||
originalUserEmail: imp.originalUserEmail || u.originalUserEmail || '',
|
||||
originalUserFullName: imp.originalUserFullName || u.originalUserFullName || '',
|
||||
originalUserCompany: imp.originalUserCompany || u.originalUserCompany || '',
|
||||
fio: ui.fio || null,
|
||||
profiles,
|
||||
};
|
||||
} else {
|
||||
// Имперсонация завершилась — убираем флаги, восстанавливаем оригинальные данные
|
||||
req.session.user = {
|
||||
...u,
|
||||
isImpersonated: false,
|
||||
impersonationType: null,
|
||||
impersonatedCompanyId: null,
|
||||
originalUserEmail: null,
|
||||
originalUserFullName: null,
|
||||
originalUserCompany: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function applyContext(req, u) {
|
||||
const isImpersonated = !!(u.originalUserEmail && u.impersonatedCompanyId);
|
||||
|
||||
req.email = u.email || '';
|
||||
// activeClientId может переопределять impersonatedCompanyId при switchTo
|
||||
req.clientId = isImpersonated
|
||||
? (u.activeClientId || u.impersonatedCompanyId || u.clientId || '')
|
||||
: (u.activeClientId || u.clientId || '');
|
||||
req.companyName = u.companyName || req.clientId;
|
||||
// Админ: пользователь с WZ01112 — всегда админ
|
||||
const allCids = u.allClientIds || [u.clientId || ''];
|
||||
req.canAdmin = allCids.includes('WZ01112') || u.clientId === 'WZ01112' || !!u.isAdmin || u.email === (process.env.ADMIN_EMAIL || '');
|
||||
req.adminMode = req.canAdmin;
|
||||
@@ -52,8 +111,6 @@ function resolveContext(req, res, next) {
|
||||
req.impersonatedBy = isImpersonated ? u.originalUserEmail : null;
|
||||
req.allClientIds = allCids;
|
||||
req.profiles = u.profiles || [];
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = { resolveContext };
|
||||
|
||||
+22
-10
@@ -100,23 +100,35 @@ function cidrToRange(cidr) {
|
||||
// Схлопывание списка CIDR в минимальный набор (для экспорта).
|
||||
// Пример: 10.0.0.0/24 + 10.0.1.0/24 → 10.0.0.0/23
|
||||
function aggregateCIDRs(cidrs) {
|
||||
// 1. Сортируем по start, мержим смежные
|
||||
const ranges = cidrs.map(c => cidrToRange(c)).sort((a, b) => a.start - b.start);
|
||||
const merged = [];
|
||||
for (const r of ranges) {
|
||||
const last = merged[merged.length - 1];
|
||||
if (last && r.start <= last.end + 1) last.end = Math.max(last.end, r.end);
|
||||
else merged.push({ ...r });
|
||||
else merged.push({ start: r.start, end: r.end });
|
||||
}
|
||||
return merged.map(r => {
|
||||
let start = r.start, end = r.end, cidrs = [];
|
||||
while (start <= end) {
|
||||
let maxMask = 32;
|
||||
while (maxMask > 0 && ((start & ((1 << (32 - maxMask)) - 1)) !== 0 || (start + (1 << (32 - maxMask)) - 1) > end)) maxMask--;
|
||||
cidrs.push(numToIP(start) + '/' + maxMask);
|
||||
start += (1 << (32 - maxMask));
|
||||
// 2. Каждый merged range → минимальный набор CIDR
|
||||
const result = [];
|
||||
for (const r of merged) {
|
||||
let start = r.start;
|
||||
while (start <= r.end) {
|
||||
// Ищем минимальную маску (самый широкий блок), подходящую под start и end.
|
||||
// Идём от 1 до 32, запоминаем минимальную (самую широкую) подходящую.
|
||||
let bestM = 32;
|
||||
for (let m = 1; m <= 32; m++) {
|
||||
if ((1 << (32 - m)) <= 0) continue;
|
||||
const size = 1 << (32 - m);
|
||||
if ((start & (size - 1)) !== 0) continue; // не выровнен
|
||||
if (start + size - 1 > r.end) continue; // выходит за range
|
||||
bestM = m;
|
||||
break; // нашли самую широкую
|
||||
}
|
||||
result.push(numToIP(start) + '/' + bestM);
|
||||
start += (1 << (32 - bestM));
|
||||
}
|
||||
return cidrs;
|
||||
}).flat();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function numToIP(n) {
|
||||
|
||||
@@ -88,10 +88,16 @@
|
||||
}
|
||||
td:last-child { border-right: none; }
|
||||
tr:last-child td { border-bottom: none; }
|
||||
tr:nth-child(even) td { background: #f1f5f9; }
|
||||
tr:nth-child(even):hover td { background: #e2e8f0; }
|
||||
tr:hover td { background: #f8fafc; }
|
||||
code { background: #f1f5f9; padding: .15rem .4rem; border-radius: 3px; font-size: .9em; }
|
||||
.empty { text-align: center; color: var(--muted); padding: 2rem; }
|
||||
|
||||
/* Help */
|
||||
.help p, .help ul { margin-top: .5rem; }
|
||||
.help ul li { margin-top: .15rem; }
|
||||
|
||||
/* Select */
|
||||
select {
|
||||
padding: .4rem .75rem; border: 1px solid var(--border); border-radius: 6px;
|
||||
@@ -256,6 +262,31 @@
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<!-- Справка -->
|
||||
<details class="help" style="background:var(--card);border:1px solid var(--border);border-radius:12px;padding:1rem;margin-bottom:1rem;">
|
||||
<summary style="cursor:pointer;font-weight:600;font-size:.95rem;color:var(--blue);">Доверенные адреса — защита вашего трафика во время DDoS-атак</summary>
|
||||
<div style="margin-top:.75rem;font-size:.85rem;line-height:1.6;color:var(--text);">
|
||||
<p>Когда на инфраструктуру провайдера идёт DDoS-атака, системы защиты автоматически фильтруют подозрительный трафик. Иногда под фильтрацию попадает и легитимный трафик, например, запросы из вашего офиса или от ваших партнёров.</p>
|
||||
<p>Чтобы этого не происходило, вы можете заранее указать адреса, которые провайдер гарантированно не заблокирует. Это называется списком доверенных адресов.</p>
|
||||
|
||||
<p style="font-weight:600;margin-top:.75rem;">Как пользоваться</p>
|
||||
<p><a href="https://whitelist.nodejsk8s.services.ngcloud.ru/" target="_blank">https://whitelist.nodejsk8s.services.ngcloud.ru/</a></p>
|
||||
<p>Зайти в сервис необходимо с учетной записью keycloak.</p>
|
||||
<p><strong>Добавить адрес.</strong> Нажмите «Добавить», введите IPv4-адрес или подсеть в формате CIDR и при желании — комментарий, чтобы не забыть, что это за адрес. Например: 203.0.113.10 или 203.0.113.0/24. Нажмите «Сохранить». Если вы вводите адрес хоста с маской подсети (например 203.0.113.5/24 вместо 203.0.113.0/24), сервис автоматически нормализует его до адреса подсети и сообщит вам об этом.</p>
|
||||
<p>Добавляйте комментарии к записям — через несколько месяцев вы скажете себе спасибо: будет сразу понятно, чей это адрес и зачем он добавлен.</p>
|
||||
<p><strong>Редактировать или удалить.</strong> Рядом с каждой записью есть кнопки редактирования и удаления. Удалённые записи перестают действовать через 5 минут.</p>
|
||||
|
||||
<p style="font-weight:600;margin-top:.75rem;">Ограничения</p>
|
||||
<ul style="padding-left:1.2rem;margin:.25rem 0;">
|
||||
<li>Только публичные IPv4-адреса. Серые адреса (192.168.x.x, 10.x.x.x, 172.16–31.x.x и другие служебные диапазоны) добавить нельзя — они не маршрутизируются в интернете и смысла в их добавлении нет.</li>
|
||||
<li>Только IPv4. IPv6-адреса и доменные имена не поддерживаются.</li>
|
||||
<li>Максимальный размер подсети — /22. Добавить префикс шире /22 (то есть /21, /20 и т.д.) нельзя.</li>
|
||||
<li>До 15 записей на компанию. Если вам нужно больше — обратитесь в поддержку.</li>
|
||||
<li>Нет дублей и пересечений. Если вы попытаетесь добавить адрес, который уже покрывается существующей записью (или наоборот), сервис предупредит об этом.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
</div>
|
||||
|
||||
<footer style="display:flex;justify-content:space-between;align-items:center;padding:.5rem 1.5rem;font-size:.72rem;color:var(--muted);">
|
||||
|
||||
Reference in New Issue
Block a user