Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e6e22fadc | ||
|
|
7049b5aaa9 | ||
|
|
f709959478 | ||
|
|
d2c621cf38 | ||
|
|
400908985e | ||
|
|
2d164fb9ee | ||
|
|
c823ae8128 | ||
|
|
b1d670a6bf | ||
|
|
d72bb84d25 | ||
|
|
fb17c83e60 | ||
|
|
f60713f127 | ||
|
|
74f805682f | ||
|
|
060b20002c | ||
|
|
24fb8cdc56 | ||
|
|
710dcfc7c9 | ||
|
|
c50ef814e2 | ||
|
|
30001062d4 | ||
|
|
92f0314a20 | ||
|
|
fa75f8317d | ||
|
|
90b6c509c0 | ||
|
|
102f4279dc | ||
|
|
b7e62f0468 | ||
|
|
7f8aae0d62 | ||
|
|
6899bc40d3 | ||
|
|
9f44281f9f | ||
|
|
e68e1a483c | ||
|
|
4cd1ab8c8e | ||
|
|
10b890437d | ||
|
|
b24cd75a9e | ||
|
|
df0dd73451 | ||
|
|
8f9fc4e3ee | ||
|
|
9e3ba1c97c | ||
|
|
eba1b4974e | ||
|
|
8deea87996 | ||
|
|
a6d64e1e6c | ||
|
|
795a41496f | ||
|
|
08abab6e36 | ||
|
|
0e1f34c913 | ||
|
|
24e0e39782 | ||
|
|
c4d592dcbe | ||
|
|
86a39c7881 | ||
|
|
f0743c6bfe | ||
|
|
c5cd577561 | ||
|
|
5941899c9c | ||
|
|
ecf6638683 | ||
|
|
e440700a72 | ||
|
|
b41af5649f | ||
|
|
bf72cb49fc | ||
|
|
235da375c4 | ||
|
|
6660cb3c77 | ||
|
|
ee0cf68fe3 | ||
|
|
3493d45065 | ||
|
|
e2aaf3f425 | ||
|
|
f215fa8e32 | ||
|
|
6b994d0938 | ||
|
|
08e20c10ac | ||
|
|
1b043713a9 | ||
|
|
799406df78 | ||
|
|
bde68c00a1 | ||
|
|
02abab93d7 | ||
|
|
2233d3f94d | ||
|
|
cb1e4fac22 | ||
|
|
dc1f629560 | ||
|
|
b640e9e8e7 | ||
|
|
afcfc1420e | ||
|
|
07356acadc | ||
|
|
b04ef35b13 | ||
|
|
394c778f46 | ||
|
|
f5ee0a36de | ||
|
|
8fe89f3a0a | ||
|
|
6bff095a67 | ||
|
|
9ca0d087de | ||
|
|
11c3817376 | ||
|
|
ebcdb1ccb1 | ||
|
|
eb3c17cd68 | ||
|
|
eb7788235f | ||
|
|
8ab0bb10ac | ||
|
|
2545bc97f1 | ||
|
|
f00b7f3e87 | ||
|
|
a31b3e61f2 | ||
|
|
2f151f4cdf | ||
|
|
2e89a71afa | ||
|
|
9eba8859cf | ||
|
|
7fa6b43562 | ||
|
|
3e418f9f4a | ||
|
|
814d066b10 | ||
|
|
cfaf8f34c3 | ||
|
|
30aaf8a7e4 | ||
|
|
e5815279a1 | ||
|
|
616b4ead09 | ||
|
|
1af304b2a8 | ||
|
|
9454e9853f | ||
|
|
fe5a7d0d37 | ||
|
|
3c061b6c4b | ||
|
|
4b5ccc6a4c | ||
|
|
5bc1b5773a | ||
|
|
0925ae4fd8 | ||
|
|
2f59c033ac | ||
|
|
81921c4e38 | ||
|
|
23c59c1aae | ||
|
|
e7f0611947 | ||
|
|
1772ade84b | ||
|
|
3f50a6fecf | ||
|
|
2a1f4add2b | ||
|
|
545d1c8aac | ||
|
|
7f7124e44e | ||
|
|
8522988151 | ||
|
|
cd49398660 | ||
|
|
53dec84045 | ||
|
|
d3b753e6bd | ||
|
|
287d196eaf | ||
|
|
a835cf0f16 | ||
|
|
df72fa2fe1 | ||
|
|
15da5cd441 | ||
|
|
b18429049e | ||
|
|
eacf90e433 | ||
|
|
33b879c098 | ||
|
|
0dc9820a31 | ||
|
|
1a2dd87705 | ||
|
|
522d2adfda | ||
|
|
e91f1b0ff3 | ||
|
|
03d480de12 | ||
|
|
53f2b49049 | ||
|
|
21a2c65296 | ||
|
|
8f8093fa29 | ||
|
|
3e4c5cb1af | ||
|
|
5aee05e8dc | ||
|
|
0733f4eaa1 | ||
|
|
ac13330fdb | ||
|
|
5c09844002 | ||
|
|
b2f1cae37d | ||
|
|
2993f95b8d | ||
|
|
199585be67 | ||
|
|
f818f454a3 | ||
|
|
bc209aeab8 | ||
|
|
a601570a4a | ||
|
|
5781b37882 | ||
|
|
ffdc12bdf8 | ||
|
|
001db2a556 | ||
|
|
447acba762 | ||
|
|
8f9e0a96ad | ||
|
|
68ea44a81d | ||
|
|
d0a0fdc264 | ||
|
|
1350f0dbba | ||
|
|
351cb6afe6 | ||
|
|
e0cbf4bf35 | ||
|
|
e3c4c738aa | ||
|
|
3bc285dc93 | ||
|
|
6025d84959 | ||
|
|
7758d70dcf | ||
|
|
9c51e449ab | ||
|
|
9a331eec82 | ||
|
|
a2cfd2c10f | ||
|
|
7b3dd8b4b3 | ||
|
|
ce6b67e72a | ||
|
|
9cb53df8b1 | ||
|
|
fa4d5646fa | ||
|
|
f739754c3b | ||
|
|
cad6ebb4d6 | ||
|
|
8b891a4bed | ||
|
|
08248229b9 | ||
|
|
2fbdc3259b | ||
|
|
d54a72ca2c | ||
|
|
4af34616f9 | ||
|
|
13466ebd37 | ||
|
|
1794610f77 | ||
|
|
2acad33fd4 | ||
|
|
4f1f64dd6c | ||
|
|
8c1787df19 | ||
|
|
02859d6a7a | ||
|
|
32cb0f4ac6 | ||
|
|
e22b7ac408 | ||
|
|
54f950b7d1 | ||
|
|
9fc3665292 | ||
|
|
4a45546b0d | ||
|
|
9b76333b22 | ||
|
|
a39f97f3b4 | ||
|
|
6c5215513c | ||
|
|
61361ec7e2 | ||
|
|
d0bfdab085 | ||
|
|
9c74479121 | ||
|
|
51b6fdf132 | ||
|
|
e3ebd8ba4e | ||
|
|
f5141f652f | ||
|
|
f40bcb84fd | ||
|
|
b0e43f981b | ||
|
|
dce2368712 | ||
|
|
9fd808bc1f | ||
|
|
393595301b | ||
|
|
ae6f3c4f8d | ||
|
|
1f1d70e577 | ||
|
|
e7aeceb3b6 | ||
|
|
4f8695aeda | ||
|
|
28954515e3 | ||
|
|
a0b66bb602 | ||
|
|
e9bd94cea3 | ||
|
|
05c6920987 | ||
|
|
3ef5478130 | ||
|
|
a3cf9812df | ||
|
|
c1df27d29d | ||
|
|
58e34042de | ||
|
|
99796202cb | ||
|
|
7b66b94774 | ||
|
|
8cbbe52316 | ||
|
|
6edef60ed8 | ||
|
|
bbcda90378 | ||
|
|
a8a07fe019 | ||
|
|
878e46287b | ||
|
|
0b3f1ab445 | ||
|
|
c10b70f97a | ||
|
|
b7fe03f02d | ||
|
|
f5dd0eaf15 | ||
|
|
8577ffa05c | ||
|
|
2194c1650f | ||
|
|
43fa0faf7a | ||
|
|
2a77b163db | ||
|
|
006741a5fa | ||
|
|
8e72fc4559 | ||
|
|
38c7adf25d | ||
|
|
22beaa86a3 | ||
|
|
2841e3ac27 | ||
|
|
8feabc339d | ||
|
|
ba0779db51 | ||
|
|
9344eb57fb | ||
|
|
3dc5dc4ecc | ||
|
|
ac0f151bcb | ||
|
|
d7afff4bcd | ||
|
|
599808b838 | ||
|
|
0fd5b43a94 | ||
|
|
805b440e5d | ||
|
|
b10b3e8b09 | ||
|
|
6bbd0fe1e2 | ||
|
|
15b458eb35 | ||
|
|
4694b7e8a6 | ||
|
|
453e25e40f | ||
|
|
51a003e078 | ||
|
|
72e021acfe | ||
|
|
9384e516ed | ||
|
|
66a52d9363 | ||
|
|
b91b78ab04 | ||
|
|
f1416e6566 | ||
|
|
ff4b35414c | ||
|
|
23d55a1005 | ||
|
|
8ea10f1e37 | ||
|
|
59ad0fa67e | ||
|
|
8ab2271ff9 | ||
|
|
1b1b3b6bd7 | ||
|
|
a083f65323 | ||
|
|
07528b0de3 | ||
|
|
94fb12b99d | ||
|
|
a3d535781c | ||
|
|
f75214c2a7 | ||
|
|
c4bb86f2fc | ||
|
|
1e581b3208 | ||
|
|
10ef2011be | ||
|
|
d1d591299e | ||
|
|
63d3a67eea | ||
|
|
64da1f9834 | ||
|
|
0abaa65bf8 | ||
|
|
bfeae74e71 | ||
|
|
707319ed23 | ||
|
|
e90b0454c1 | ||
|
|
5e1ce80003 | ||
|
|
35d8b7fcc2 | ||
|
|
d8613c7112 | ||
|
|
a24634199b | ||
|
|
d8de91577c | ||
|
|
c0c10a5ea0 | ||
|
|
0413b44051 | ||
|
|
d81430dc9a | ||
|
|
b0a5875f4e | ||
|
|
ea4551ce13 | ||
|
|
02d842fd73 | ||
|
|
5c286af43b | ||
|
|
a355b34e4a | ||
|
|
fa67872f30 | ||
|
|
c19480336d | ||
|
|
a6177c4cef | ||
|
|
8531a0b545 | ||
|
|
6cf4bc0b44 | ||
|
|
86b782bc33 | ||
|
|
ed5b3c8367 | ||
|
|
7779bf416a | ||
|
|
aef7da5740 | ||
|
|
d31b8952b4 | ||
|
|
c85a438ba2 | ||
|
|
87216389e3 | ||
|
|
09bb56c6f4 | ||
|
|
7d2b6a309f | ||
|
|
1e330a4395 | ||
|
|
91d18afe10 | ||
|
|
1f5c0d8623 | ||
|
|
8ab7eed629 | ||
|
|
e97c36f19f | ||
|
|
093b96b9ae | ||
|
|
0845440f86 | ||
|
|
97c3f66b3c |
@@ -1,16 +1,38 @@
|
||||
# База данных
|
||||
DB_HOST=your-db-host.svc.cluster.local
|
||||
# ── База данных PostgreSQL ──
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_NAME=postgres
|
||||
DB_USER=your-user
|
||||
DB_PASS=your-password
|
||||
DB_SSLMODE=require
|
||||
DB_NAME=ipwhitelist
|
||||
DB_USER=ipwhitelist
|
||||
DB_PASS=<openssl rand -base64 24>
|
||||
DB_SSLMODE=disable
|
||||
|
||||
# Сервер
|
||||
PORT=3000
|
||||
|
||||
# Лимиты
|
||||
# ── Режим разработки ──
|
||||
# true = mock-логин (без Keycloak, для тестов)
|
||||
# false = OIDC SSO через Keycloak (ОБЯЗАТЕЛЬНО в production)
|
||||
DEV_MODE=true
|
||||
DEFAULT_LIMIT=15
|
||||
|
||||
# Режим разработки (true = без Keycloak, читать X-Dev-User из заголовка)
|
||||
DEV_MODE=true
|
||||
# ── Сессия (сгенерировать: openssl rand -base64 32) ──
|
||||
SESSION_SECRET=<openssl rand -base64 32>
|
||||
CSRF_SECRET=<openssl rand -base64 32>
|
||||
|
||||
# ── Порт сервера ──
|
||||
PORT=3001
|
||||
|
||||
# ── Администратор (clientId) ──
|
||||
ADMIN_CLIENT_ID=WZ01112
|
||||
|
||||
# ── IAM API (сервис авторизации Nubes) ──
|
||||
# Используется для получения списка компаний пользователя (GET /api/v1/auth/user)
|
||||
# и переключения активной компании (POST /api/v1/user/switch-profile).
|
||||
# ⚠️ ВЫБЕРИТЕ СТЕНД (раскомментируйте нужный):
|
||||
# Dev: https://auth-api-dev.ngcloud.ru
|
||||
# Test: https://auth-api-test.ngcloud.ru
|
||||
# Prod: https://auth-api.ngcloud.ru
|
||||
IAM_API_URL=https://auth-api-dev.ngcloud.ru
|
||||
|
||||
# ── Keycloak / OIDC (ЗАПОЛНИТЬ при DEV_MODE=false) ──
|
||||
# KC_CLIENT_ID= # ID зарегистрированного OIDC-клиента в Keycloak
|
||||
# KC_CLIENT_SECRET= # секрет клиента
|
||||
# KC_BASE_URL=https://keycloak.nubes.ru/realms/cloud
|
||||
# APP_URL=https://ваш-домен.ru
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
# IP Whitelist App — AI Agent Guide
|
||||
|
||||
> For AI coding agents (Copilot, etc.) to understand and continue development.
|
||||
> Last updated: 2026-06-04 | Version: 0.5.23
|
||||
|
||||
## 1. Quick Overview
|
||||
|
||||
**Purpose:** Self-service portal for cloud provider customers to manage trusted IPv4 whitelist entries (CIDRs) that are excluded from DDoS filtering.
|
||||
|
||||
**Stack:** Node.js + Express + EJS + PostgreSQL
|
||||
**Auth:** Mock RS256 JWT (dev) / Keycloak OIDC (production)
|
||||
**Deployment:** PM2, behind nginx on a VM
|
||||
|
||||
## 2. Architecture
|
||||
|
||||
```
|
||||
Browser → nginx:443 → Express:3001
|
||||
├── /export (public, no auth)
|
||||
├── /api/v1/* (Bearer JWT, REST API)
|
||||
└── /* (UI layer, SSR EJS, session-based)
|
||||
```
|
||||
|
||||
### Two-layer design:
|
||||
- **API layer** (`src/api/routes/`): REST endpoints, Bearer JWT auth, direct DB access
|
||||
- **UI layer** (`ui/routes/`): SSR EJS pages, session-based auth, proxies to API via HTTP (`ui/api-client.js`)
|
||||
|
||||
### Why two layers?
|
||||
- API can be used directly by nginx/firewall for whitelist export
|
||||
- UI provides human-friendly management interface
|
||||
- Clear separation of concerns
|
||||
|
||||
### Key flow:
|
||||
```
|
||||
Browser → UI route → api-client.js (HTTP) → API route → PostgreSQL
|
||||
```
|
||||
|
||||
## 3. Project Structure
|
||||
|
||||
```
|
||||
ipwhitelist-app/
|
||||
├── server.js # Entry point — mounts all routes
|
||||
├── package.json
|
||||
├── .env # Config (not in git)
|
||||
├── .env.example # Config template
|
||||
├── sql/schema.sql # Database schema
|
||||
│
|
||||
├── src/
|
||||
│ ├── auth.js # Auth: mock JWT + OIDC/Keycloak
|
||||
│ ├── config.js # MOCK_USERS + helpers
|
||||
│ ├── db.js # PostgreSQL pool
|
||||
│ ├── queries.js # All DB queries (CRUD, export, audit)
|
||||
│ ├── validators.js # CIDR validation + blocked ranges + aggregation
|
||||
│ │
|
||||
│ ├── api/
|
||||
│ │ ├── index.js # API router mount
|
||||
│ │ └── routes/
|
||||
│ │ ├── entries.js # GET/POST/PATCH/DELETE /entries + /entries/export
|
||||
│ │ └── admin.js # GET /companies, PATCH /limit, GET /audit
|
||||
│ │
|
||||
│ ├── middleware/
|
||||
│ │ ├── csrf.js # CSRF protection (NOT ACTIVE — tokens are empty)
|
||||
│ │ ├── rateLimit.js # Rate limiters (auth, mutation, export)
|
||||
│ │ └── session.js # express-session with pg store
|
||||
│ │
|
||||
│ └── routes/
|
||||
│ ├── oidc.js # OIDC/Keycloak routes (inactive in mock mode)
|
||||
│ └── (old monolithic routes deleted in v0.5.11)
|
||||
│
|
||||
├── ui/
|
||||
│ ├── index.js # UI middleware: requireToken + resolveUser + mount routers
|
||||
│ ├── api-client.js # HTTP client to /api/v1/* (from UI layer)
|
||||
│ └── routes/
|
||||
│ ├── auth.js # GET/POST /login, POST /login-token, GET /logout
|
||||
│ ├── entries.js # GET /, POST /add, POST /edit/:id, POST /delete/:id
|
||||
│ ├── admin.js # GET /admin, /audit (admin-only)
|
||||
│ └── export.js # GET /export (proxies to API)
|
||||
│
|
||||
├── views/
|
||||
│ ├── index.ejs # Main page: entries table + add/edit/delete forms
|
||||
│ ├── ui-login.ejs # Login page (mock + manual clientId entry)
|
||||
│ ├── admin.ejs # Admin: companies list + limits
|
||||
│ ├── audit.ejs # Audit log
|
||||
│ └── error.ejs # 404/500 error page
|
||||
│
|
||||
├── tests/
|
||||
│ ├── api.js # 121 API integration tests (Bearer JWT)
|
||||
│ ├── integration.js # 68 UI integration tests (session, EJS)
|
||||
│ ├── tz-compliance.js # 47 ТЗ compliance tests
|
||||
│ ├── tz-full-compliance.js # 111 comprehensive ТЗ tests (20 companies × 100 CIDRs)
|
||||
│ ├── run-tests.js # 68 unit/smoke tests (no DB)
|
||||
│ └── stress.js # 191 concurrency/stress tests
|
||||
│
|
||||
└── docs/
|
||||
├── ТЗ.md # Tech spec (Russian)
|
||||
├── ТЗ-плюс.md # Client clarifications
|
||||
├── ТЗ-реализация.md # Implementation notes
|
||||
├── keycloak-auth-reference.md # Keycloak/OIDC reference
|
||||
├── devops-deploy.md # DevOps deployment guide
|
||||
└── deploy-keycloak.md # Keycloak setup guide
|
||||
```
|
||||
|
||||
## 4. Auth System
|
||||
|
||||
### Mock Mode (default, DEV_MODE=true)
|
||||
- Local RSA keypair generated at startup
|
||||
- `POST /login-token` with `clientId` field → mock JWT → session
|
||||
- Mock users defined in `src/config.js`:
|
||||
- `admin` (WZ01112) — admin@nubes.ru
|
||||
- `alfa` (WZ88888) — alfa@example.com
|
||||
- `multi` (WZ88888, WZ77777) — multi@example.com
|
||||
- `beta` (WZ77777) — beta@example.com
|
||||
|
||||
### OIDC Mode (production, when KC_CLIENT_ID set)
|
||||
- Authorization Code flow via Keycloak
|
||||
- Routes in `src/routes/oidc.js` (inactive in mock mode)
|
||||
- JWKS certificate verification
|
||||
|
||||
### UI resolveUser (ui/index.js)
|
||||
- Decodes JWT from session (no signature check — API validates on each call)
|
||||
- Extracts `ClientID`, `allClientIds`, `activeClientId`, `companyId`, `companyName`, `email`
|
||||
- Sets `req.user` for all downstream handlers
|
||||
|
||||
### API bearerMiddleware (src/auth.js)
|
||||
- Verifies Bearer JWT signature (RS256 via JWKS or mock keypair)
|
||||
- Sets `req.user` via `userFromPayload()`
|
||||
|
||||
## 5. Multi-Company
|
||||
|
||||
Users can belong to multiple companies via IAM API profiles:
|
||||
- User's companies come from IAM API (`profiles[]`) with `is_active_profile` flag
|
||||
- Active company selector in UI (dropdown, `?switchTo=X` or JS navigation)
|
||||
- API supports `?client_id=<id>` parameter to specify active company
|
||||
- Default: first company in list (`allClientIds[0]`)
|
||||
|
||||
**IMPORTANT:** `req.user.activeClientId` must be synced BOTH in `session.user.activeClientId` AND `req.user.activeClientId` — see `ui/routes/entries.js` switch handler.
|
||||
|
||||
## 6. Company Isolation
|
||||
|
||||
- Each company is a row in `companies` table (unique by `client_id`)
|
||||
- Entries are isolated by `company_id` foreign key
|
||||
- Admin can view/edit any company via `?company=<db_id>`
|
||||
- User sees only their company's entries
|
||||
- Multi-company user switches via `?client_id=<clientId>`
|
||||
|
||||
## 7. Database
|
||||
|
||||
Three tables (see `sql/schema.sql`):
|
||||
- `companies` — (id, client_id, name, custom_limit, created_at, updated_at)
|
||||
- `whitelist_entries` — (id, company_id FK, value_cidr, comment, created_by, created_at, updated_at, deleted_at, deleted_by)
|
||||
- `audit_log` — (id, user_email, company_id, action, old_value, new_value, entry_id, created_at)
|
||||
|
||||
Default limit: 15 entries per company (configurable via `DEFAULT_LIMIT` env).
|
||||
|
||||
## 8. Key Endpoints
|
||||
|
||||
| Method | Path | Auth | Description |
|
||||
|--------|------|------|-------------|
|
||||
| GET | `/export` | None (public) | Aggregated CIDR list, text/plain |
|
||||
| GET | `/api/v1/entries` | Bearer JWT | List entries for company |
|
||||
| POST | `/api/v1/entries` | Bearer JWT | Create entry |
|
||||
| PATCH | `/api/v1/entries/:id` | Bearer JWT | Update entry |
|
||||
| DELETE | `/api/v1/entries/:id` | Bearer JWT | Soft-delete entry |
|
||||
| GET | `/api/v1/entries/export` | Bearer JWT | Aggregated CIDR export |
|
||||
| GET | `/api/v1/companies` | Bearer JWT (admin) | List companies |
|
||||
| PATCH | `/api/v1/companies/:id/limit` | Bearer JWT (admin) | Set company limit |
|
||||
| GET | `/api/v1/audit` | Bearer JWT (admin) | Audit log |
|
||||
| GET | `/login` | None | Login page (UI) |
|
||||
| POST | `/login` | None | Mock login (form) |
|
||||
| POST | `/login-token` | None | Manual clientId login |
|
||||
| GET | `/logout` | Session | Logout |
|
||||
|
||||
### Export query params:
|
||||
- `?view=1` — inline (browser display)
|
||||
- `?filename=X` — custom filename (default: white-list.txt)
|
||||
|
||||
## 9. CIDR Validation Rules
|
||||
|
||||
- Only IPv4, no IPv6
|
||||
- Mask must be /22 to /32 (inclusive)
|
||||
- Single IP without mask = /32
|
||||
- 14 blocked ranges (RFC1918, CGNAT, loopback, etc.)
|
||||
- No duplicates within a company
|
||||
- No overlaps within a company
|
||||
- Normalization: host bits zeroed (e.g., 9.9.9.5/24 → 9.9.9.0/24)
|
||||
- Comment max 255 chars, optional
|
||||
|
||||
## 10. Running Tests
|
||||
|
||||
```bash
|
||||
# Unit tests (no DB needed)
|
||||
node tests/run-tests.js
|
||||
|
||||
# API tests (requires DB, DEV_MODE=true)
|
||||
node tests/api.js
|
||||
|
||||
# UI integration tests
|
||||
node tests/integration.js
|
||||
|
||||
# ТЗ compliance
|
||||
node tests/tz-compliance.js
|
||||
|
||||
# Full ТЗ compliance (20 companies × 100 CIDRs)
|
||||
node tests/tz-full-compliance.js
|
||||
|
||||
# Stress/concurrency tests
|
||||
node tests/stress.js
|
||||
```
|
||||
|
||||
All tests: 347+ PASS, 0 FAIL
|
||||
|
||||
## 11. Deployment
|
||||
|
||||
Deploy the app on a VM with nginx as reverse proxy. See `docs/devops-deploy.md` for details.
|
||||
|
||||
## 12. Common Pitfalls
|
||||
|
||||
### 🚨 Rate Limiter
|
||||
`authLimiter`: 10 requests per 5 minutes. Hits quickly during testing. Reset after 5 min or use different IPs.
|
||||
|
||||
### 🚨 Mock Keypair Mismatch
|
||||
Each `initAuth()` generates a NEW RSA keypair. When testing with supertest, the UI api-client makes HTTP requests to `localhost:3001` (PM2 instance), which has a DIFFERENT keypair → "invalid signature". Fix: test against the running PM2 instance directly, or stop PM2 and use the supertest port.
|
||||
|
||||
### 🚨 Company Switch Bug (FIXED v0.5.12)
|
||||
`resolveUser` runs BEFORE the route handler. When switching companies via `?switchTo=X`, the session was updated but `req.user.activeClientId` still had the old value. Fixed by adding `req.user.activeClientId = switchTo` after session update.
|
||||
|
||||
### 🚨 CSRF Not Active
|
||||
`csrfToken` is always `""` in templates. CSRF middleware is not mounted. Forms work, but there's no CSRF protection. To enable: mount `doubleCsrfProtection` middleware and call `generateCsrfToken()` in GET handlers.
|
||||
|
||||
### 🚨 `.env` on VM gets overwritten
|
||||
Rsync excludes `.env` now, but double-check. If `.env` contains DB credentials, they MUST NOT be overwritten.
|
||||
|
||||
### 🚨 Version Must Always Increment
|
||||
After ANY code change: bump `"version"` in `package.json`. PM2 shows version — user expects it to match.
|
||||
|
||||
## 13. OIDC/Keycloak Transition
|
||||
|
||||
To enable production auth:
|
||||
1. DevOps creates client in Keycloak with redirect URI `/callback`
|
||||
2. Set `.env`: `KC_CLIENT_ID`, `KC_CLIENT_SECRET`, `KC_BASE_URL`, `APP_URL`
|
||||
3. Set `DEV_MODE=false`
|
||||
4. `pm2 restart ipwhitelist --update-env`
|
||||
5. Routes in `src/routes/oidc.js` activate automatically when `KC_CLIENT_ID` is set
|
||||
|
||||
See `docs/deploy-keycloak.md` for details.
|
||||
|
||||
## 14. Version History
|
||||
|
||||
| Version | Date | Key Changes |
|
||||
|---------|------|-------------|
|
||||
| 0.5.23 | 2026-06-04 | 0.0.0.0/8 blocked, security+edge test suites |
|
||||
| 0.5.22 | 2026-06-04 | 4-audit cleanup, gitignore hardening |
|
||||
| 0.5.21 | 2026-06-04 | Header injection fix, API timeout, nginx duplicate |
|
||||
| 0.5.20 | 2026-06-04 | IAM fallback warning, fail-fast IAM_API_URL, X-Forwarded-Proto |
|
||||
| 0.5.19 | 2026-06-04 | safeReturn in all redirects (open redirect fix) |
|
||||
| 0.5.18 | 2026-06-04 | IAM API integration: fetchIamUser, switchProfile, profiles[] |
|
||||
| 0.5.17 | 2026-06-04 | Previous stable |
|
||||
| 0.5.14 | 2026-06-03 | Token login, JWKS in mock mode, production deploy docs |
|
||||
| 0.5.13 | 2026-06-03 | OIDC routes prepared, .env.example, devops-deploy guide, repo cleanup |
|
||||
| 0.5.12 | 2026-06-02 | Fixed company switch bug, aggregation, validation, TZ compliance |
|
||||
| 0.5.11 | 2026-06-02 | Dead code cleanup, new integration tests, /export public |
|
||||
| 0.5.10 | 2026-06-02 | UI/API split, mock users fixed, 121 API tests passing |
|
||||
@@ -0,0 +1,29 @@
|
||||
# IP WhiteList — Copilot Instructions
|
||||
|
||||
## Ключевые правила
|
||||
1. Отвечать кратко. Вопрос = СТОП.
|
||||
2. Не трогать рабочий код без «делай».
|
||||
3. Не лезть в БД / kubectl / сервер без прямого указания.
|
||||
4. После любой правки — VERSION++ в package.json.
|
||||
5. Git только локально: add + commit + push.
|
||||
6. ⛔ ПЕРЕД ЛЮБЫМ ИЗМЕНЕНИЕМ КОДА — описать что именно будет сделано, получить «делай».
|
||||
|
||||
## Репа
|
||||
- https://gitea.services.ngcloud.ru/Nail/ipwhitelist-app.git
|
||||
|
||||
## Стек
|
||||
- Node.js + Express + EJS
|
||||
- PostgreSQL
|
||||
- REST API /api/v1/* (Bearer JWT)
|
||||
- UI-слой (сессии, SSR через EJS)
|
||||
|
||||
## Тесты
|
||||
- `node tests/api.js` — API (Bearer JWT, 121 тест)
|
||||
- `node tests/integration.js` — интеграционные (сессии, HTML-формы)
|
||||
|
||||
## Важные файлы
|
||||
- `docs/ТЗ.md` — техническое задание
|
||||
- `docs/ТЗ-плюс.md` — уточнения от заказчика
|
||||
- `docs/keycloak-auth-reference.md` — справка по Keycloak/OIDC
|
||||
- `.env.example` — шаблон конфигурации
|
||||
- `.github/AGENT-GUIDE.md` — полный гайд для AI-агентов (англ.)
|
||||
@@ -1,7 +1,55 @@
|
||||
.env
|
||||
# Система
|
||||
node_modules/
|
||||
.env
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.DS_Store
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Креды, токены, конфиги
|
||||
token*.*
|
||||
Files/
|
||||
kubeconfig-*
|
||||
|
||||
# Внутренняя аналитика (не для публичного доступа)
|
||||
AGENT.md
|
||||
AGENT-DIAGNOSIS.md
|
||||
PROJECT-AUDIT.md
|
||||
CONTEXT.md
|
||||
SESSION-SUMMARY.md
|
||||
prompt-*.md
|
||||
docs/PLAN-*.md
|
||||
docs/STATE-*.md
|
||||
docs/cluster-cleanup-*.md
|
||||
docs/tests-*.md
|
||||
docs/test-api.sh
|
||||
docs/nubes-design-system.md
|
||||
docs/deploy-vm.md
|
||||
docs/questions-devops.md
|
||||
|
||||
# Локальный Keycloak (бинарники не коммитим)
|
||||
keycloak/keycloak-*/
|
||||
keycloak/data/
|
||||
keycloak/*.tar.gz
|
||||
|
||||
# Промпты для внешних агентов
|
||||
prompt-*.txt
|
||||
|
||||
# Результаты код-ревью агентов
|
||||
research/
|
||||
audits/
|
||||
docs/agent-opinions.md
|
||||
|
||||
# Логи тестов
|
||||
test-results/
|
||||
|
||||
# Тестовые окружения (только для внутреннего использования)
|
||||
.env.test
|
||||
.env.production
|
||||
DEPLOY-TESTING.md
|
||||
tests/comprehensive.js
|
||||
|
||||
# Промпты для внешних агентов
|
||||
prompt-*.txt
|
||||
docs/sonnet-*.md
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
# IP WhiteList v0.5.23
|
||||
|
||||
Self-service портал для управления доверенными IPv4-адресами клиентов облачного провайдера.
|
||||
Записи исключаются из блокировки системами фильтрации во время DDoS-атак.
|
||||
|
||||
**[[ТЗ](docs/ТЗ.md)]** · **[[DevOps — production deploy](docs/devops-deploy.md)]** · **[[Кратко (без воды)](docs/wowater.md)]**
|
||||
|
||||
---
|
||||
|
||||
## Возможности
|
||||
|
||||
- **Управление белым списком:** добавление, редактирование, удаление (soft delete) IPv4/CIDR
|
||||
- **Валидация** на сервере: маски `/22`–`/32`, запрет 14 приватных/служебных диапазонов, запрет дубликатов и пересечений, нормализация host-битов
|
||||
- **Лимиты:** глобальный (по умолчанию 15) и индивидуальный на компанию (настраивает админ)
|
||||
- **Агрегированный экспорт** CIDR-списка для систем фильтрации (`GET /export`)
|
||||
- **Мульти-компания:** через IAM API — переключение между компаниями пользователя
|
||||
- **Аудит:** все изменения фиксируются — кто, когда, что было и что стало
|
||||
- **Администрирование:** просмотр всех компаний, управление лимитами, журнал аудита
|
||||
|
||||
## Аутентификация
|
||||
|
||||
| Режим | Описание |
|
||||
|---|---|
|
||||
| **DEV_MODE=true** (тесты/разработка) | Mock JWT, локальная RSA-пара. Вход через форму выбора пользователя или вставку Bearer-токена |
|
||||
| **DEV_MODE=false** (production) | OIDC SSO через Keycloak Nubes. Редирект на Keycloak login → `/callback` → сессия |
|
||||
|
||||
В production требуются `KC_CLIENT_ID` + `KC_CLIENT_SECRET` от DevOps. Подробнее — [deploy guide](docs/devops-deploy.md).
|
||||
|
||||
## Стек
|
||||
|
||||
```
|
||||
Node.js 20+ → Express 4 → EJS (SSR) → PostgreSQL 16+
|
||||
jsonwebtoken RS256 / JWKS (Keycloak Nubes)
|
||||
express-session + connect-pg-simple
|
||||
```
|
||||
|
||||
## Архитектура
|
||||
|
||||
```
|
||||
Browser → nginx:443 → Express:3001
|
||||
├── /export (публичный CIDR)
|
||||
├── /api/v1/* (REST API, Bearer JWT)
|
||||
└── /* (UI, SSR EJS, сессия → api-client.js → API)
|
||||
```
|
||||
|
||||
Два слоя: SSR UI для браузера, JSON API для внешних систем.
|
||||
|
||||
## Multi-company
|
||||
|
||||
Пользователь может принадлежать нескольким компаниям. Список компаний и активная определяются через IAM API (`GET /api/v1/auth/user` → `profiles[]`). Переключение — через UI или `POST /api/v1/user/switch-profile`.
|
||||
|
||||
## Тесты
|
||||
|
||||
```bash
|
||||
npm test # 121 API-тест (Bearer JWT, CRUD, изоляция, admin)
|
||||
node tests/integration.js # 68 UI-тестов (сессии, EJS)
|
||||
node tests/tz-compliance.js # 47 тестов ТЗ
|
||||
node tests/tz-full-compliance.js # 111 тестов (20 компаний × 100 CIDR)
|
||||
node tests/stress.js # 191 стресс-тест (concurrency, auth attacks, edge cases)
|
||||
```
|
||||
|
||||
Все тесты требуют реального PostgreSQL.
|
||||
|
||||
## Быстрый старт (dev)
|
||||
|
||||
```bash
|
||||
git clone https://gitea.services.ngcloud.ru/Nail/ipwhitelist-app.git
|
||||
cd ipwhitelist-app
|
||||
cp .env.example .env # заполнить DB_* и SESSION_SECRET
|
||||
npm install
|
||||
psql -U postgres -d ipwhitelist -f sql/schema.sql
|
||||
npm start # http://localhost:3000 + DEV_MODE=true
|
||||
```
|
||||
|
||||
## Production deploy
|
||||
|
||||
См. [`docs/devops-deploy.md`](docs/devops-deploy.md) — nginx, HTTPS, PM2, Keycloak SSO.
|
||||
|
||||
## Документация
|
||||
|
||||
- [Техническое задание](docs/ТЗ.md)
|
||||
- [Уточнения ТЗ](docs/ТЗ-плюс.md)
|
||||
- [DevOps — production deploy](docs/devops-deploy.md)
|
||||
- [AI Agent Guide](.github/AGENT-GUIDE.md) (англ., для Copilot)
|
||||
- [Copilot Instructions](.github/copilot-instructions.md) (правила для AI)
|
||||
- [Keycloak/OIDC reference](docs/keycloak-auth-reference.md)
|
||||
- [Deploy Keycloak](docs/deploy-keycloak.md)
|
||||
|
||||
## Текущий деплой
|
||||
|
||||
- **URL:** `https://italo.kube5s.ru`
|
||||
- **Режим:** DEV_MODE=true (mock)
|
||||
- **БД:** PostgreSQL на ВМ, localhost only
|
||||
@@ -0,0 +1,130 @@
|
||||
# Техническое задание
|
||||
## Микросервис управления доверенными адресами клиентов (Белые списки IP)
|
||||
|
||||
---
|
||||
|
||||
# 1. Назначение
|
||||
|
||||
Клиенты облачного провайдера через личный кабинет управляют своими доверенными IPv4-адресами. Эти адреса исключаются из блокировки при DDoS-атаках.
|
||||
|
||||
Администраторы Нубеса могут просматривать и редактировать списки всех клиентов.
|
||||
|
||||
---
|
||||
|
||||
# 2. Авторизация
|
||||
|
||||
## 2.1. Вход
|
||||
|
||||
Пользователь входит через Keycloak (OIDC). После входа приложение запрашивает IAM API — сервис авторизации экосистемы Nubes.
|
||||
|
||||
IAM возвращает:
|
||||
- email пользователя
|
||||
- список компаний, к которым он принадлежит
|
||||
- активную компанию
|
||||
- флаг администратора (isAdmin)
|
||||
|
||||
## 2.2. Роли
|
||||
|
||||
| Роль | Возможности |
|
||||
|---|---|
|
||||
| Клиент | Видит и редактирует только записи своих компаний |
|
||||
| Администратор Нубеса | Видит и редактирует записи всех компаний, меняет лимиты, смотрит аудит, выгружает списки |
|
||||
|
||||
Администратором считается пользователь с `isAdmin = true` в IAM и `clientId = WZ01112`. Если у пользователя несколько компаний, он переключается между ними.
|
||||
|
||||
## 2.3. Режим администратора
|
||||
|
||||
Пользователь Нубеса видит переключатель «Администратор». В обычном режиме интерфейс как у клиента. При включении — выпадающий список всех компаний, ссылки на аудит и управление лимитами.
|
||||
|
||||
---
|
||||
|
||||
# 3. Управление записями
|
||||
|
||||
## 3.1. Просмотр
|
||||
|
||||
Клиент видит таблицу записей своей компании. Администратор выбирает компанию из списка.
|
||||
|
||||
Для каждой записи показывается: адрес/подсеть, комментарий, кто добавил, даты создания и изменения. Удалённые записи отображаются зачёркнутыми и полупрозрачными, без кнопок редактирования. Кнопки «Изменить» и «Удалить» — в строке таблицы.
|
||||
|
||||
Счётчик: «X из N записей».
|
||||
|
||||
## 3.2. Добавление
|
||||
|
||||
Форма: IPv4-адрес или CIDR, комментарий (необязательно, до 255 символов).
|
||||
|
||||
Проверки:
|
||||
- Формат: только IPv4, одиночный адрес или подсеть /22–/32
|
||||
- Нормализация: хост-биты обнуляются, пользователь уведомляется
|
||||
- Запрещённые диапазоны: 14 служебных подсетей (RFC1918, CGNAT, loopback и др.)
|
||||
- Нет дубликатов и пересечений внутри компании
|
||||
- Лимит компании (по умолчанию 15)
|
||||
|
||||
При превышении лимита — блокировка с сообщением.
|
||||
|
||||
## 3.3. Редактирование
|
||||
|
||||
Изменение адреса и комментария. Все проверки — как при добавлении.
|
||||
|
||||
## 3.4. Удаление
|
||||
|
||||
Логическое (soft delete): запись помечается удалённой, но физически сохраняется. Освобождает место в лимите. Исключается из выгрузки.
|
||||
|
||||
---
|
||||
|
||||
# 4. Лимиты
|
||||
|
||||
- По умолчанию: 15 записей на компанию
|
||||
- Администратор задаёт индивидуальный лимит компании (больше или меньше)
|
||||
- Снижение лимита не удаляет существующие записи, но блокирует новые
|
||||
|
||||
---
|
||||
|
||||
# 5. Экспорт
|
||||
|
||||
Администратор выгружает агрегированный список всех активных CIDR.
|
||||
|
||||
- По умолчанию — JSON: `{ cidrs: [...], count: N }`
|
||||
- `?format=txt` — текстовый файл, одна подсеть на строку
|
||||
- `?company=<id>` — выгрузка по конкретной компании
|
||||
|
||||
---
|
||||
|
||||
# 6. Аудит
|
||||
|
||||
Все изменения (создание, редактирование, удаление) пишутся в журнал. Записи неизменяемы.
|
||||
|
||||
Состав записи: кто сделал, когда, компания, действие, старое и новое значение.
|
||||
|
||||
При имперсонации добавляется поле «имперсонировал» — кто на самом деле совершил действие.
|
||||
|
||||
Журнал доступен только администратору. Фильтр по компании.
|
||||
|
||||
---
|
||||
|
||||
# 7. Имперсонация
|
||||
|
||||
Администратор IAM может «притворяться» другим пользователем или компанией. Приложение получает флаг `is_impersonated` из IAM.
|
||||
|
||||
При активной имперсонации:
|
||||
- Жёлтый баннер: «Режим имперсонации — от имени Компания (вы: originalUserEmail)»
|
||||
- CRUD идёт в контексте имперсонируемой компании
|
||||
- Аудит: `created_by = имперсонируемый`, `impersonated_by = реальный админ`
|
||||
|
||||
Без имперсонации `impersonated_by = NULL`.
|
||||
|
||||
---
|
||||
|
||||
# 8. Интерфейс
|
||||
|
||||
- Главная страница: таблица записей, форма добавления, переключатель компаний
|
||||
- Аудит: `/audit` — журнал с фильтром по компании
|
||||
- Лимиты: `/admin` — список компаний с лимитами
|
||||
- Экспорт: `/export` — выгрузка списка
|
||||
|
||||
Шапка: логотип, версия приложения, email пользователя, переключатель «Администратор», кнопка «Выйти».
|
||||
|
||||
---
|
||||
|
||||
# 9. Технологии
|
||||
|
||||
Node.js, Express, EJS, PostgreSQL, Keycloak OIDC, IAM API.
|
||||
@@ -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
|
||||
@@ -0,0 +1,156 @@
|
||||
Техническое задание
|
||||
Микросервис управления доверенными адресами клиентов
|
||||
Self-service портал для указания клиентами доверенных IPv4-адресов и подсетей,
|
||||
исключаемых из блокировки на стороне облачного провайдера во время DDoS-атак
|
||||
|
||||
───────────────────────────────────────────────────────────────
|
||||
КРАТКОЕ ОПИСАНИЕ (пояснение к реализации)
|
||||
───────────────────────────────────────────────────────────────
|
||||
Сервис даёт клиентам облачного провайдера личный кабинет, где они сами указывают
|
||||
свои доверенные IPv4-адреса и подсети. Эти адреса провайдер не блокирует во время
|
||||
DDoS-атак — так легитимный трафик клиента не попадает под ложные срабатывания
|
||||
фильтрации.
|
||||
|
||||
Что реализуется:
|
||||
|
||||
Личный кабинет клиента. Клиент входит через привычную авторизацию (Keycloak),
|
||||
видит свой список доверенных адресов и управляет им сам: добавляет, редактирует,
|
||||
удаляет записи с комментариями. Если пользователь работает с несколькими
|
||||
компаниями — переключается между ними.
|
||||
|
||||
Проверка вводимых данных. Форма принимает только корректные IPv4-адреса и подсети,
|
||||
отклоняет «серые» и служебные диапазоны, не допускает дубликатов и пересечений
|
||||
внутри одной компании.
|
||||
|
||||
Ограничение по количеству. На компанию по умолчанию 15 записей. Лимит
|
||||
настраивается глобально, а для отдельной компании администратор может поднять или
|
||||
опустить его индивидуально.
|
||||
|
||||
Режим администратора (сетевые инженеры провайдера). Единое окно, где видны записи
|
||||
всех компаний, с фильтрами и доступом к истории изменений. Администратор управляет
|
||||
лимитами и при необходимости любыми записями.
|
||||
|
||||
История изменений (аудит). Каждое создание, изменение и удаление фиксируется: кто,
|
||||
когда, что именно изменил. Удаление — логическое, данные физически сохраняются.
|
||||
|
||||
Выдача для систем фильтрации. Отдельный адрес, по которому системы защиты
|
||||
автоматически забирают итоговый сводный список всех доверенных адресов (одним
|
||||
txt-файлом, по строке на запись). Адреса при этом схлопываются в компактный общий
|
||||
перечень.
|
||||
───────────────────────────────────────────────────────────────
|
||||
1. Назначение и цели
|
||||
1.1. Назначение
|
||||
Микросервис предоставляет клиентам облачного провайдера web-интерфейс для самостоятельного управления списком доверенных IPv4-адресов и подсетей. Записи из этого списка исключаются из автоматической блокировки сетевого взаимодействия системами фильтрации и митигации провайдера, что снижает количество ложноположительных срабатываний для легитимного трафика клиента.
|
||||
1.2. Цели
|
||||
Дать клиентам возможность самостоятельно поддерживать актуальный список доверенных IPv4-адресов, которые будут исключаться из фильтрации во время DDoS-атак.
|
||||
Предоставить сетевым инженерам единую точку просмотра и управления списками доверенных клиентских белых IPv4-адресов.
|
||||
Обеспечить машиночитаемую выдачу агрегированного (суммаризированного) списка для систем фильтрации трафика.
|
||||
2. Объем работ
|
||||
Web-страница / закладка в личном кабинете для управления whitelist-записями.
|
||||
Авторизация через существующий экземпляр Keycloak (OIDC).
|
||||
Валидация формы на стороне клиента и сервера.
|
||||
Внешний endpoint выдачи агрегированного списка. Выдача txt-файлом с переносом строки. Одна строка – один объект.
|
||||
Хранение записей, журнал аудита.
|
||||
Административное управление лимитами по компаниям.
|
||||
3. Роли и права доступа
|
||||
Роли определяются на основании claims в OIDC-токене Keycloak. Соответствие claim → роль настраивается на этапе развёртывания.
|
||||
Роль
|
||||
Идентификация
|
||||
Видимость записей
|
||||
Права на изменение
|
||||
Клиент (client)
|
||||
clientId
|
||||
Только записи компаний, к которым принадлежит пользователь.
|
||||
Создание, редактирование и удаление записей своих компаний (в пределах лимита).
|
||||
Администратор (admin)
|
||||
clientId = WZ01112 (Нубес) и отдельный чек-бокс
|
||||
Записи всех компаний.
|
||||
Создание, редактирование, удаление всех записей. Изменение лимита для отдельных компаний.
|
||||
3.1. Принадлежность к компании
|
||||
Принадлежность пользователя к компании определяется из claim токена. Поддерживается сценарий, когда пользователь принадлежит нескольким компаниям: в этом случае в интерфейсе предусматривается переключатель активной компании, а все операции выполняются в контексте выбранной компании.
|
||||
Ожидаемые claims (имена согласуются с командой Keycloak):
|
||||
clientID — идентификатор компании
|
||||
email — идентификация пользователя для аудита
|
||||
4. Функциональные требования
|
||||
4.1. Просмотр списка записей
|
||||
Клиент видит таблицу записей активной компании; Администратор – записи всех компаний с фильтром по компании.
|
||||
Для каждой записи отображаются: значение (адрес/подсеть), комментарий (если есть), автор(email), дата создания, дата последнего изменения.
|
||||
Soft-deleted записи по умолчанию скрыты; для администратора предусмотрен фильтр для их отображения.
|
||||
Отображается текущее использование лимита: «использовано X из N».
|
||||
4.2. Создание записи
|
||||
Форма содержит поля: значение (IPv4-адрес или подсеть CIDR) и необязательный комментарий (до 255 символов).
|
||||
Значение проходит валидацию (см. раздел 5) на клиенте и обязательно повторно на сервере.
|
||||
Перед сохранением проверяется: соблюдение лимита компании, отсутствие пересечений и дубликатов внутри компании, отсутствие принадлежности к запрещённым диапазонам.
|
||||
При успешном сохранении создаётся запись аудита.
|
||||
4.3. Редактирование записи
|
||||
Редактирование значения и комментария доступно компании в рамках своих прав.
|
||||
При изменении значения повторно выполняется полный набор проверок валидации и пересечений.
|
||||
Изменение фиксируется в журнале аудита с сохранением прежнего и нового значения.
|
||||
4.4. Удаление записи (soft delete)
|
||||
Удаление выполняется как логическое (soft delete): запись помечается удалённой (deleted_at, deleted_by), но физически сохраняется.
|
||||
Удалённая запись освобождает место в лимите компании и исключается из внешней агрегированной выдачи.
|
||||
Действие фиксируется в журнале аудита.
|
||||
4.5. Лимит записей на компанию
|
||||
Действует глобальный лимит по умолчанию: 15 активных записей на компанию.
|
||||
Значение глобального лимита по умолчанию задаётся конфигурацией сервиса и может быть изменено без пересборки.
|
||||
Для отдельной компании администратор может задать индивидуальный лимит, переопределяющий глобальный (как в большую, так и в меньшую сторону).
|
||||
При попытке превысить лимит создание блокируется с понятным сообщением; в подсчёт идут только активные записи.
|
||||
Снижение лимита ниже текущего числа записей не удаляет существующие записи, но блокирует создание новых до приведения в соответствие.
|
||||
4.6. Журнал аудита
|
||||
Все изменяющие операции фиксируются неизменяемыми записями аудита.
|
||||
Каждая запись аудита содержит: кто (пользователь), когда (timestamp), компания, тип действия, прежнее и новое состояние.
|
||||
Журнал доступен для просмотра только администратору.
|
||||
4.7. Внешняя выдача агрегированного списка
|
||||
Подсети суммаризируются (агрегируются в минимальный набор CIDR) по всем компаниям совместно. Пересечения между разными компаниями допустимы.
|
||||
Предоставляется отдельный HTTP GET endpoint, отдающий полный суммаризированный список активных записей всех компаний файлом в формате txt.
|
||||
Авторизация: на старте endpoint может работать без авторизации (по сетевому ограничению / разрешенный список потребителей по ip).
|
||||
5. Требования к валидации
|
||||
Валидация выполняется на клиенте и обязательно дублируется на сервере. Серверная валидация является авторитетной.
|
||||
Правило
|
||||
Описание
|
||||
Формат IPv4
|
||||
Допускается одиночный адрес (например 203.0.113.10) или подсеть в нотации CIDR (например 203.0.113.0/24). Допускается использование масок /32 - /22. Маска /21 и больше не допускается.
|
||||
Только IPv4
|
||||
IPv6-значения или доменные имена отклоняются.
|
||||
Корректность подсети
|
||||
Введенный адрес с маской подсети должен нормализоваться к адресу подсети, все host-биты должны быть обнулены.
|
||||
Пользователь должен быть уведомлен, что ввел адрес из хостовой части, а не адрес подсети и произошла нормализация.
|
||||
Запрет серых адресов
|
||||
Адреса и подсети из частных диапазонов (Приложение А) запрещены к добавлению.
|
||||
Отсутствие дубликатов
|
||||
В пределах одной компании запрещены полностью совпадающие записи.
|
||||
Отсутствие пересечений
|
||||
В пределах одной компании запрещено добавление записи, пересекающейся с уже существующей (включая вложенность подсетей). Между разными компаниями пересечения допускаются.
|
||||
Длина комментария
|
||||
Не более 255 символов; поле необязательное.
|
||||
Приложение А – Список запрещенных к созданию подсетей.
|
||||
Назначение
|
||||
Префикс
|
||||
Private (RFC1918)
|
||||
10.0.0.0/8
|
||||
Private (RFC1918)
|
||||
172.16.0.0/12
|
||||
Private (RFC1918)
|
||||
192.168.0.0/16
|
||||
CGNAT (RFC6598)
|
||||
100.64.0.0/10
|
||||
Loopback
|
||||
127.0.0.0/8
|
||||
Link-local (APIPA)
|
||||
169.254.0.0/16
|
||||
IANA special block
|
||||
192.0.0.0/24
|
||||
TEST-NET-1 (docs)
|
||||
192.0.2.0/24
|
||||
TEST-NET-2 (docs)
|
||||
198.51.100.0/24
|
||||
TEST-NET-3 (docs)
|
||||
203.0.113.0/24
|
||||
Benchmarking
|
||||
198.18.0.0/15
|
||||
Multicast
|
||||
224.0.0.0/4
|
||||
Reserved (Class E)
|
||||
240.0.0.0/4
|
||||
Limited broadcast
|
||||
255.255.255.255/32
|
||||
@@ -0,0 +1,68 @@
|
||||
# Переход на Keycloak SSO (Nubes)
|
||||
|
||||
## Как сейчас
|
||||
- `DEV_MODE=true` — mock-аутентификация
|
||||
- Любой clientId можно ввести вручную на `/login`
|
||||
- JWT подписывается локальным RSA-ключом
|
||||
|
||||
## Что нужно от DevOps (v0.5.23)
|
||||
|
||||
### 1. Параметры для .env (на ВМ)
|
||||
|
||||
```bash
|
||||
# ── Отключить mock-режим (убрать DEV_MODE или поставить false) ──
|
||||
# DEV_MODE=true
|
||||
|
||||
# ── Keycloak realm (прямой или через API Gateway) ──
|
||||
KC_BASE_URL=https://keycloak.nubes.ru/realms/cloud
|
||||
# ИЛИ через API Gateway:
|
||||
# KC_BASE_URL=<url API Gateway>
|
||||
|
||||
# ── OAuth client (зарегистрированное приложение) ──
|
||||
KC_CLIENT_ID=<ID клиента из Keycloak>
|
||||
KC_CLIENT_SECRET=<секрет клиента>
|
||||
|
||||
# ── Публичный URL приложения (для редиректа из Keycloak) ──
|
||||
APP_URL=https://<ваш-домен>
|
||||
|
||||
# ── JWKS URL для верификации токенов (если не через KC_BASE_URL) ──
|
||||
# JWKS_URL=https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/certs
|
||||
```
|
||||
|
||||
### 2. Уточнить
|
||||
|
||||
| Вопрос | Описание |
|
||||
|---|---|
|
||||
| **API Gateway?** | Токен получать напрямую из Keycloak или через API Gateway? |
|
||||
| **ClientID в токене** | Какой claim содержит ID компании? Сейчас ждём `ClientID` или `client_id`. |
|
||||
| **Admin-роль** | Как определяется admin? `clientId === WZ01112` достаточно или нужен отдельный claim? |
|
||||
| **Redirect URI** | Какой зарегистрирован? `https://<домен>/callback`? |
|
||||
| **Несколько компаний** | Компании через IAM API (GET /auth/user → profiles[]) |
|
||||
|
||||
### 3. Что сделано в коде (готово)
|
||||
|
||||
| Компонент | Файл |
|
||||
|---|---|
|
||||
| JWKS-загрузка + кеширование | `src/auth.js` |
|
||||
| Верификация RS256 JWT | `src/auth.js` |
|
||||
| `buildAuthUrl()` — редирект на Keycloak | `src/auth.js` |
|
||||
| `exchangeCode()` — обмен code→token | `src/auth.js` |
|
||||
| `buildLogoutUrl()` — logout | `src/auth.js` |
|
||||
| OIDC-роуты /login, /callback, /logout | `src/routes/oidc.js` |
|
||||
| `userFromPayload()` — парсинг claims | `src/auth.js` |
|
||||
|
||||
### 4. Порядок включения
|
||||
|
||||
```bash
|
||||
# 1. Настроить .env на ВМ (параметры выше)
|
||||
# 2. Перезапустить
|
||||
pm2 restart ipwhitelist --update-env
|
||||
|
||||
# 3. Проверить что включился OIDC-режим
|
||||
pm2 logs ipwhitelist --lines 3
|
||||
# Должно быть: "[auth] OIDC — Keycloak"
|
||||
# Если видно "[auth] MOCK" — значит KC_CLIENT_ID не задан
|
||||
|
||||
# 4. Открыть https://<домен>/login
|
||||
# Должен редиректить на Keycloak вместо формы входа
|
||||
```
|
||||
@@ -0,0 +1,285 @@
|
||||
# Деплой ipwhitelist — PRODUCTION
|
||||
|
||||
Репозиторий: `https://gitea.services.ngcloud.ru/Nail/ipwhitelist-app.git` (публичный)
|
||||
ОС: Ubuntu 22.04 / 24.04 (для других ОС команды могут отличаться)
|
||||
|
||||
---
|
||||
|
||||
## 0. Что должно быть готово до начала
|
||||
|
||||
| Что | Откуда берётся |
|
||||
|---|---|
|
||||
| Сервер (ВМ) с Ubuntu | Выделяет DevOps |
|
||||
| Домен (например `white.nubes.ru`) | DNS запись на сервер |
|
||||
| Доступ к Keycloak Nubes (realm `cloud`) | Существующий Keycloak в инфраструктуре Nubes |
|
||||
| Зарегистрированный OIDC-клиент в Keycloak | Создаётся в Keycloak (шаг 6) |
|
||||
| `KC_CLIENT_ID` и `KC_CLIENT_SECRET` | Выдаются после регистрации клиента |
|
||||
|
||||
**Что будет установлено на сервер:** Node.js 20, PostgreSQL 16, nginx, certbot, PM2.
|
||||
|
||||
---
|
||||
|
||||
## 1. PostgreSQL — установка
|
||||
|
||||
**Где:** на этой же ВМ, рядом с приложением.
|
||||
**Версия:** 16 (на Ubuntu 22.04 — 14, на 24.04 — 16).
|
||||
**Безопасность:** слушает только localhost (наружу не торчит).
|
||||
|
||||
```bash
|
||||
# 1.1 Установить PostgreSQL
|
||||
sudo apt update
|
||||
sudo apt install -y postgresql
|
||||
|
||||
# 1.2 Проверить что запустился и слушает localhost
|
||||
sudo systemctl status postgresql --no-pager
|
||||
ss -tlnp | grep 5432
|
||||
# Должно быть: 127.0.0.1:5432 (НЕ 0.0.0.0:5432)
|
||||
|
||||
# 1.3 Сгенерировать пароль для БД — он понадобится в .env
|
||||
DB_PASS=$(openssl rand -base64 24)
|
||||
echo "Пароль БД: $DB_PASS" # сохранить — вписать в .env
|
||||
|
||||
# 1.4 Создать пользователя и базу данных
|
||||
sudo -u postgres psql -c "CREATE USER ipwhitelist WITH PASSWORD '${DB_PASS}';"
|
||||
sudo -u postgres psql -c "CREATE DATABASE ipwhitelist OWNER ipwhitelist;"
|
||||
```
|
||||
|
||||
**Таблицы создадим позже (шаг 2.3), после клонирования кода — схема лежит в репозитории.**
|
||||
|
||||
---
|
||||
|
||||
## 2. Node.js + код приложения + таблицы БД
|
||||
|
||||
```bash
|
||||
# 2.1 Установить Node.js 20
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo bash -
|
||||
sudo apt install -y nodejs
|
||||
|
||||
# 2.2 Скачать код и установить зависимости
|
||||
sudo git clone https://gitea.services.ngcloud.ru/Nail/ipwhitelist-app.git /opt/ipwhitelist
|
||||
sudo chown -R $(whoami):$(whoami) /opt/ipwhitelist # чтобы npm install не просил sudo
|
||||
cd /opt/ipwhitelist
|
||||
npm install --production
|
||||
|
||||
# 2.3 Теперь код есть — можно создать таблицы (schema.sql в репозитории)
|
||||
sudo -u postgres psql -d ipwhitelist -f /opt/ipwhitelist/sql/schema.sql
|
||||
|
||||
# 2.4 Проверить что таблицы создались
|
||||
sudo -u postgres psql -d ipwhitelist -c "\dt"
|
||||
# Результат: companies, whitelist_entries, audit_log, session
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. .env — настройка приложения
|
||||
|
||||
```bash
|
||||
# 3.1 Скопировать шаблон
|
||||
cd /opt/ipwhitelist
|
||||
sudo cp .env.example .env
|
||||
sudo chmod 600 .env # чтобы никто не прочитал пароли
|
||||
```
|
||||
|
||||
**3.2 Открыть .env и заполнить значения:**
|
||||
```bash
|
||||
sudo nano /opt/ipwhitelist/.env
|
||||
```
|
||||
|
||||
**Что и куда писать:**
|
||||
|
||||
| Строка в .env | Что вписать |
|
||||
|---|---|
|
||||
| `DB_PASS=` | Пароль, сгенерированный на шаге 1.3 |
|
||||
| `DEV_MODE=false` | **ОБЯЗАТЕЛЬНО false** иначе будет mock-логин |
|
||||
| `ADMIN_CLIENT_ID=WZ01112` | clientId администратора (получить у заказчика) |
|
||||
| `KC_CLIENT_ID=` | ID клиента из Keycloak (шаг 6) |
|
||||
| `KC_CLIENT_SECRET=` | Secret клиента из Keycloak (шаг 6) |
|
||||
| `APP_URL=https://white.nubes.ru` | **Реальный домен** без слеша на конце |
|
||||
| `SESSION_SECRET=` | Сгенерировать: `openssl rand -base64 32` |
|
||||
| `CSRF_SECRET=` | Сгенерировать: `openssl rand -base64 32` |
|
||||
| `IAM_API_URL=` | **Выбрать стенд**: `auth-api-dev` (dev), `auth-api-test` (test), `auth-api` (prod). По умолчанию dev — сменить для production! |
|
||||
|
||||
**Остальные строки не трогать** — значения по умолчанию подходят.
|
||||
|
||||
---
|
||||
|
||||
## 4. nginx — HTTPS и прокси
|
||||
|
||||
**Зачем:** браузер не пускает на HTTPS-сайты с localhost. Нужен nginx как прослойка.
|
||||
|
||||
```bash
|
||||
# 4.1 Установить nginx и certbot
|
||||
sudo apt install -y nginx certbot python3-certbot-nginx
|
||||
```
|
||||
|
||||
**4.2 Создать конфиг nginx.** Заменить `white.nubes.ru` на свой домен:
|
||||
|
||||
```bash
|
||||
sudo nano /etc/nginx/sites-available/ipwhitelist
|
||||
```
|
||||
|
||||
Вставить:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name white.nubes.ru; # ← ЗАМЕНИТЬ на свой домен
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name white.nubes.ru; # ← ЗАМЕНИТЬ на свой домен
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/white.nubes.ru/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/white.nubes.ru/privkey.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:3001;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
# 4.3 Включить сайт
|
||||
sudo ln -s /etc/nginx/sites-available/ipwhitelist /etc/nginx/sites-enabled/
|
||||
sudo rm -f /etc/nginx/sites-enabled/default
|
||||
|
||||
# 4.4 Получить SSL-сертификат (certbot сам подправит конфиг)
|
||||
sudo certbot --nginx -d white.nubes.ru # ← ЗАМЕНИТЬ на свой домен
|
||||
|
||||
# 4.5 Проверить что nginx работает
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. PM2 — автозапуск приложения
|
||||
|
||||
**Зачем:** чтобы приложение само стартовало после перезагрузки сервера.
|
||||
|
||||
```bash
|
||||
# 5.1 Установить PM2
|
||||
sudo npm i -g pm2
|
||||
|
||||
# 5.2 Запустить приложение
|
||||
cd /opt/ipwhitelist
|
||||
sudo pm2 start server.js --name ipwhitelist --update-env
|
||||
|
||||
# 5.3 Настроить автозапуск при перезагрузке
|
||||
sudo pm2 startup systemd
|
||||
sudo pm2 save
|
||||
|
||||
# 5.4 Проверить что запустилось
|
||||
sudo pm2 status
|
||||
# Должен быть статус: online
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Keycloak — регистрация OIDC-клиента
|
||||
|
||||
**Где:** в админке Keycloak Nubes (`https://keycloak.nubes.ru/admin`, realm `cloud`).
|
||||
**Зачем:** чтобы Keycloak знал наше приложение и пускал пользователей обратно после входа.
|
||||
|
||||
**6.1 Создать клиента:**
|
||||
|
||||
| Поле | Значение |
|
||||
|---|---|
|
||||
| Client ID | `white.nubes.ru` (или свой домен) |
|
||||
| Client authentication | ON (включено) |
|
||||
| Standard flow | ON (включено) |
|
||||
| Valid redirect URIs | `https://white.nubes.ru/callback` |
|
||||
| Web origins | `https://white.nubes.ru` |
|
||||
|
||||
**6.2 Забрать секрет:** после создания клиента открыть вкладка `Credentials` → `Client secret` → скопировать.
|
||||
|
||||
**6.3 Создать mapper'ы** (вкладка `Client scopes` → `<ClientID>-dedicated` → `Add mapper` → `By configuration` → `User Attribute`):
|
||||
|
||||
Создать три mapper'а:
|
||||
|
||||
| Имя | User Attribute | Claim name | Тип |
|
||||
|---|---|---|---|
|
||||
| `ClientID` | `client_id` | `ClientID` | String |
|
||||
| `company_id` | `company_id` | `company_id` | String |
|
||||
| `company_name` | `company_name` | `company_name` | String |
|
||||
|
||||
**6.4 Создать пользователей** (вкладка `Users` → `Add user`):
|
||||
|
||||
| Логин | Пароль | Атрибуты |
|
||||
|---|---|---|
|
||||
| `admin` | задать вкладка Credentials | `client_id=WZ01112`, `company_id=1`, `company_name=Nubes Admin` |
|
||||
| `client` | задать | `client_id=WZ88888`, `company_id=2`, `company_name=Альфа` |
|
||||
|
||||
**Как задать атрибуты:** вкладка `Attributes` → добавить по одному → `Save`.
|
||||
|
||||
**Multi-company:** список компаний пользователя — через IAM API (`GET /api/v1/auth/user → profiles[]`). Активная компания определяется по `is_active_profile`. Дополнительных атрибутов в Keycloak не требуется.
|
||||
|
||||
---
|
||||
|
||||
## 7. Финальная проверка
|
||||
|
||||
```bash
|
||||
# 7.1 Убедиться что в .env всё заполнено
|
||||
grep -E "DEV_MODE|KC_CLIENT_ID|KC_CLIENT_SECRET|APP_URL|DB_PASS" /opt/ipwhitelist/.env
|
||||
# Проверить: DEV_MODE=false, KC_CLIENT_ID и KC_CLIENT_SECRET не пустые
|
||||
|
||||
# 7.2 Перезапустить приложение
|
||||
cd /opt/ipwhitelist
|
||||
sudo pm2 restart ipwhitelist --update-env
|
||||
|
||||
# 7.3 Проверить что включился OIDC-режим
|
||||
sudo pm2 logs ipwhitelist --lines 3 --nostream
|
||||
# В логе должно быть: [auth] OIDC — Keycloak
|
||||
# Если [auth] MOCK — значит KC_CLIENT_ID/KC_CLIENT_SECRET не заданы
|
||||
```
|
||||
|
||||
**Открыть в браузере:**
|
||||
|
||||
| URL | Что должно произойти |
|
||||
|---|---|
|
||||
| `https://white.nubes.ru/healthz` | Покажет `OK` |
|
||||
| `https://white.nubes.ru/` | Редирект на страницу логина Keycloak |
|
||||
| Ввести логин/пароль пользователя из Keycloak | Редирект обратно на главную с белым списком |
|
||||
| `https://white.nubes.ru/export` | Список CIDR (текст) |
|
||||
| `https://white.nubes.ru/logout` | Выход из системы |
|
||||
|
||||
---
|
||||
|
||||
## 8. Обновление приложения
|
||||
|
||||
```bash
|
||||
cd /opt/ipwhitelist
|
||||
sudo git pull
|
||||
sudo npm install --production
|
||||
sudo pm2 restart ipwhitelist --update-env
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Переменные .env — полный список
|
||||
|
||||
| Переменная | Когда нужна | По умолчанию | Что писать |
|
||||
|---|---|---|---|
|
||||
| `DB_HOST` | всегда | — | `localhost` |
|
||||
| `DB_PORT` | всегда | 5432 | — |
|
||||
| `DB_NAME` | всегда | — | `ipwhitelist` |
|
||||
| `DB_USER` | всегда | — | `ipwhitelist` |
|
||||
| `DB_PASS` | всегда | — | пароль из шага 1.3 |
|
||||
| `DB_SSLMODE` | никогда | disable | не трогать |
|
||||
| `DEV_MODE` | всегда | true | **false** в production |
|
||||
| `ADMIN_CLIENT_ID` | всегда | WZ01112 | clientId админа |
|
||||
| `KC_CLIENT_ID` | для SSO | — | из Keycloak (шаг 6) |
|
||||
| `KC_CLIENT_SECRET` | для SSO | — | из Keycloak (шаг 6) |
|
||||
| `KC_BASE_URL` | для SSO | `https://keycloak.nubes.ru/realms/cloud` | не трогать |
|
||||
| `APP_URL` | для SSO | — | `https://white.nubes.ru` |
|
||||
| `SESSION_SECRET` | всегда | — | `openssl rand -base64 32` |
|
||||
| `CSRF_SECRET` | опционально | — | `openssl rand -base64 32` |
|
||||
| `PORT` | всегда | 3000 | `3001` |
|
||||
| `DEFAULT_LIMIT` | опционально | 15 | не трогать |
|
||||
| `API_BASE` | всегда | `http://localhost:PORT` | не трогать |
|
||||
@@ -0,0 +1,128 @@
|
||||
# Сессия 2026-06-11 — Имперсонация, toggle, аудит
|
||||
|
||||
## Хронология
|
||||
|
||||
### 1. Починка toggle «Администратор»
|
||||
**Проблема:** чекбокс не показывался.
|
||||
**Причина:** `canAdminMode` вычислялся в `resolveUser`, но не передавался в `res.render`.
|
||||
**Исправление:** добавлен `canAdminMode: req.user.canAdminMode` в `entries.js`.
|
||||
|
||||
### 2. Дубликат catch → крэш
|
||||
При правке `ui/index.js` остался дубликат `} catch {`. Приложение падало при старте.
|
||||
**Исправление:** удалён лишний catch.
|
||||
|
||||
### 3. JS onclick → form POST
|
||||
Toggle не реагировал — сложный inline JS с `getComputedStyle` и `fetch`.
|
||||
**Исправление:** заменён на `<form method="POST" action="/toggle-admin">`. Ноль JS.
|
||||
|
||||
### 4. Чужой email в header
|
||||
Админ видел `nstasyuk@nubes.ru` вместо своего — шаблон показывал `selectedCompany.owner_email`.
|
||||
**Исправление:** всегда показывать `user.email`.
|
||||
|
||||
### 5. Хардкод isNail
|
||||
IAM не давал `isAdmin: true` для `ntazetdinov@nubes.ru`.
|
||||
**Исправление:** временный хардкод `isNail` в `ui/index.js`.
|
||||
|
||||
### 6. canWrite из IAM
|
||||
IAM возвращает `can_write: false` для всех — это IAM-шная RBAC, не наш флаг.
|
||||
**Исправление:** `canWrite` убран полностью.
|
||||
|
||||
### 7. Имперсонация: флаги из IAM
|
||||
Добавлены поля из IAM: `isImpersonated`, `originalUserEmail`, `originalUserFullName`, `impersonationType`.
|
||||
**Файлы:** `src/auth.js:fetchIamUser`, `src/routes/oidc.js`, `ui/index.js:resolveUser`.
|
||||
|
||||
### 8. Баннер имперсонации
|
||||
В `views/index.ejs` добавлен жёлтый баннер при `isImpersonated`.
|
||||
|
||||
### 9. Двойной аудит
|
||||
Колонка `impersonated_by` в `audit_log`.
|
||||
**Файлы:** `src/db.js` (миграция), `src/queries.js:logAudit`, `src/api/routes/entries.js`.
|
||||
|
||||
### 10. X-Imp-* заголовки — мост UI→API
|
||||
UI-слой знал про имперсонацию, API-слой — нет.
|
||||
**Исправление:** `api-client.js:impHeaders()`, `bearerMiddleware` читает `X-Imp-Email`, `X-Imp-OriginalEmail`, `X-Imp-ClientId`.
|
||||
|
||||
### 11. Безопасность: OIDC state check
|
||||
`|| true` в `/callback` отключал проверку state во всех окружениях.
|
||||
**Исправление:** убрано `|| true`.
|
||||
|
||||
### 12. Безопасность: open redirect
|
||||
`POST /login-token` редиректил без `safeReturn`.
|
||||
**Исправление:** добавлен `safeReturn(returnTo)`.
|
||||
|
||||
### 13. Безопасность: client_id без проверки
|
||||
При пустом `allowedIds` можно было подставить любой `?client_id=`.
|
||||
**Исправление:** `requestedId` принимается только если `allowedIds.length > 0`.
|
||||
|
||||
### 14. Тестовая имперсонация
|
||||
Три env-переменные: `IMPERSONATION_ORIGINAL`, `IMPERSONATION_TARGET`, `IMPERSONATION_COMPANY`.
|
||||
**Файл:** `ui/index.js` (блок `TEST IMPERSONATION`).
|
||||
|
||||
### 15. Часовой пояс GMT+4
|
||||
`Europe/Moscow` → `Etc/GMT-4` в `views/index.ejs` и `views/audit.ejs`.
|
||||
|
||||
### 16. Аудит: колонка «Имперсонировал»
|
||||
В `views/audit.ejs` добавлена колонка `impersonated_by`. Фильтр по компании — кнопка вместо JS.
|
||||
|
||||
### 17. Удалённые записи: визуал
|
||||
Зачёркивание + полупрозрачность. Кнопки скрыты для удалённых.
|
||||
**Файл:** `views/index.ejs`.
|
||||
|
||||
### 18. Чекбокс «Показать удалённые»
|
||||
Возвращён в админ-режим.
|
||||
**Файл:** `views/index.ejs`.
|
||||
|
||||
### 19. ТЗ-факт
|
||||
Составлен документ `TZs/ТЗ-факт.md` — техзадание по фактически реализованному функционалу.
|
||||
|
||||
### 20. Сверка с требованиями Сергея
|
||||
Прочитан `Files/wsm.txt`. Подтверждено соответствие. Не сделано: ссылка на запрещённые префиксы, текст-мануал от Стасюка.
|
||||
|
||||
## Основные файлы, изменённые в сессии
|
||||
|
||||
| Файл | Что |
|
||||
|---|---|
|
||||
| `ui/index.js` | canAdminMode, isNail, TEST IMPERSONATION, canWrite |
|
||||
| `ui/routes/entries.js` | canAdminMode в render, impHeaders, упрощение ownCo |
|
||||
| `ui/routes/auth.js` | toggle-admin redirect, open redirect fix |
|
||||
| `ui/api-client.js` | impHeaders(), X-Imp-* заголовки |
|
||||
| `src/auth.js` | fetchIamUser (все поля), bearerMiddleware (X-Imp-*), safeReturn |
|
||||
| `src/routes/oidc.js` | isImpersonated в сессию, fix || true |
|
||||
| `src/api/routes/entries.js` | impersonatedBy в queries, client_id fix, export JSON |
|
||||
| `src/db.js` | ALTER TABLE impersonated_by |
|
||||
| `src/queries.js` | logAudit с impersonatedBy |
|
||||
| `views/index.ejs` | toggle (form POST), баннеры, удалённые записи, includeDeleted |
|
||||
| `views/audit.ejs` | колонка impersonated_by, кнопка фильтра, GMT+4 |
|
||||
| `server.js` | debug-session с lastAudit |
|
||||
|
||||
## Ветки
|
||||
- `save-2026-06-11` — снапшот перед критичными фиксами
|
||||
- `save-2026-06-12` — финальное состояние
|
||||
|
||||
---
|
||||
|
||||
## Что осталось (для следующего чата)
|
||||
|
||||
### Баги
|
||||
1. **Toggle виден при имперсонации** — по требованию Сергея: «Когда я имперсонирую — вижу то же что пользователь». При имперсонации `canAdminMode` должен быть false (сейчас true, потому что вычисляется из оригинального JWT, а не из `activeClientId`). `ui/index.js` строка 65-67.
|
||||
2. **Email в header берётся из `payload.email`**, а не из `req.session?.user?.email`. Иногда показывает чужой email. `ui/index.js` строка 75.
|
||||
|
||||
### Не сделано по ТЗ/требованиям Сергея
|
||||
3. Ссылка на список зарезервированных префиксов (14 диапазонов)
|
||||
4. Краткий мануал на главной для пользователей (ждём от Стасюка)
|
||||
|
||||
### Временные решения (убрать при настройке IAM)
|
||||
5. `isNail` — хардкод `ntazetdinov@nubes.ru` как админ. Заменить на `isAdmin` из IAM. `ui/index.js` строка 63.
|
||||
6. `TEST IMPERSONATION` — env-переменные. Убрать для прода. `ui/index.js` строки 70-78.
|
||||
|
||||
### Архитектурный долг
|
||||
7. Дублирование `buildSessionUser` в `oidc.js` и `auth.js`
|
||||
8. Флаги `isAdmin`/`adminMode`/`canAdminMode`/`isNail` — упростить до двух
|
||||
9. Циклическая зависимость `ui → src/auth` через `require('../../src/auth')`
|
||||
|
||||
## Как работать дальше
|
||||
|
||||
1. Прочитать ТЗ: `TZs/ТЗ-факт.md` (фактическое), `docs/ТЗ.md` (оригинал)
|
||||
2. Прочитать требования Сергея: `Files/wsm.txt` (особенно строки 378-470)
|
||||
3. Начать с бага №1 (toggle + имперсонация) — это противоречит требованию заказчика
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
# Сессия 2026-06-12
|
||||
|
||||
## Архитектурный анализ
|
||||
|
||||
**Вердикт: модульный монолит (modular monolith)**
|
||||
|
||||
Не чистый монолит, не микросервисы. Осмысленная архитектура с DI и разделением на слои.
|
||||
|
||||
### Слои
|
||||
|
||||
```
|
||||
UI (EJS) → HTTP → API (JSON) → Queries → PostgreSQL
|
||||
↑
|
||||
DI через фабрики:
|
||||
createApiRouter({ auth, q })
|
||||
```
|
||||
|
||||
UI не ходит в БД напрямую — всегда через `ui/api-client.js` → HTTP → `/api/v1/*`. Разделение по протоколу.
|
||||
|
||||
### Степень связанности
|
||||
|
||||
| Модуль | Связанность | Почему |
|
||||
|--------|-------------|--------|
|
||||
| `validators.js` | Ноль | Только `net`, чистые функции |
|
||||
| `config.js` | Ноль | Чистые данные |
|
||||
| `api-client.js` | Ноль | Только `http`/`https` |
|
||||
| `bearerAuth.js` | Ноль | Standalone middleware |
|
||||
| `queries.js` → `db.js` | Tight | Data-access — иначе быть не может |
|
||||
| `server.js` → всё | Tight | Оркестратор — нормально |
|
||||
|
||||
### Что хорошо
|
||||
|
||||
- DI-паттерн: роутеры получают зависимости через фабрики
|
||||
- Нет циклических зависимостей — граф чистый DAG
|
||||
- Большинство модулей тестируемы изолированно
|
||||
- Лёгкий вынос API в микросервис: поменять `API_BASE` в `api-client.js`
|
||||
|
||||
### Что не очень
|
||||
|
||||
- `server.js` знает про все модули (15 импортов) — но для монолита норма
|
||||
- `src/auth.js` делает и JWT, и IAM, и bearer — можно разделить
|
||||
- Дублирование `buildSessionUser` в `oidc.js` и `auth.js`
|
||||
|
||||
### Граф зависимостей
|
||||
|
||||
```
|
||||
server.js
|
||||
├── src/db.js → pg
|
||||
├── src/middleware/session.js → express-session, connect-pg-simple
|
||||
├── src/middleware/csp.js
|
||||
├── src/middleware/rateLimit.js
|
||||
├── src/auth.js → src/config.js
|
||||
├── src/queries.js → src/db.js, src/validators.js
|
||||
├── src/validators.js → net
|
||||
├── src/config.js (ноль зависимостей)
|
||||
├── src/api/index.js → src/api/routes/* → src/validators.js
|
||||
│ └── src/api/middleware/bearerAuth.js (ноль зависимостей)
|
||||
├── ui/index.js → ui/routes/* → ui/api-client.js (только http/https)
|
||||
│ └── ui/routes/auth.js → src/auth.js (только safeReturn)
|
||||
└── src/routes/oidc.js
|
||||
```
|
||||
@@ -0,0 +1,83 @@
|
||||
# IAM Impersonation API
|
||||
|
||||
> Источник: Swagger `https://auth-api-dev.ngcloud.ru/api/v1/documentation/`
|
||||
> Дата: 2026-06-16
|
||||
|
||||
## Все эндпоинты имперсонации
|
||||
|
||||
| Метод | Путь | Описание |
|
||||
|-------|------|----------|
|
||||
| `POST` | `/api/v1/impersonation/start` | Начать имперсонацию |
|
||||
| `POST` | `/api/v1/impersonation/end` | Завершить имперсонацию |
|
||||
| `POST` | `/api/v1/impersonation/extend` | Продлить сессию имперсонации |
|
||||
| `GET` | `/api/v1/impersonation/status` | Статус текущей имперсонации |
|
||||
| `GET` | `/api/v1/impersonation/history` | История своих имперсонаций |
|
||||
| `GET` | `/api/v1/impersonation/history/all` | История имперсонаций всех админов |
|
||||
|
||||
## Формат ImpersonationStartRequest
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "user",
|
||||
"entity_id": "UUID контакта (contactId из userInfo)",
|
||||
"reason": "описание причины"
|
||||
}
|
||||
```
|
||||
|
||||
- `entity_id` — **UUID таргета** (кого имперсонируем), не свой
|
||||
- `type` — всегда `"user"`
|
||||
|
||||
## curl-команды
|
||||
|
||||
### Начать имперсонацию
|
||||
|
||||
```bash
|
||||
curl --http2 -X POST \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Accept: application/json" \
|
||||
"https://auth-api.ngcloud.ru/api/v1/impersonation/start" \
|
||||
-d '{"type":"user","entity_id":"UUID_таргета","reason":"тест"}'
|
||||
```
|
||||
|
||||
### Статус
|
||||
|
||||
```bash
|
||||
curl --http2 \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Accept: application/json" \
|
||||
"https://auth-api.ngcloud.ru/api/v1/impersonation/status"
|
||||
```
|
||||
|
||||
### История
|
||||
|
||||
```bash
|
||||
curl --http2 \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Accept: application/json" \
|
||||
"https://auth-api.ngcloud.ru/api/v1/impersonation/history"
|
||||
```
|
||||
|
||||
### Продлить
|
||||
|
||||
```bash
|
||||
curl --http2 -X POST \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Accept: application/json" \
|
||||
"https://auth-api.ngcloud.ru/api/v1/impersonation/extend"
|
||||
```
|
||||
|
||||
### Завершить
|
||||
|
||||
```bash
|
||||
curl --http2 -X POST \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Accept: application/json" \
|
||||
"https://auth-api.ngcloud.ru/api/v1/impersonation/end"
|
||||
```
|
||||
|
||||
## Примечания
|
||||
|
||||
- Требуется роль IAM-админа или право на объект `impersonation`
|
||||
- `--http2` обязателен для обхода ddos-guard
|
||||
- Стенды: `auth-api.ngcloud.ru` (prod), `auth-api-test.ngcloud.ru` (test), `auth-api-dev.ngcloud.ru` (dev)
|
||||
@@ -0,0 +1,480 @@
|
||||
# IAM API — интеграция с ipwhitelist-app
|
||||
|
||||
> Дата: 2026-06-04
|
||||
> Исследование: проверка текущей интеграции и план доработок
|
||||
|
||||
---
|
||||
|
||||
## 1. Что такое IAM API
|
||||
|
||||
IAM (Identity & Access Management) — единый сервис авторизации экосистемы Nubes. Через него работают Личный Кабинет (ЛК) и все смежные сервисы.
|
||||
|
||||
**Три стенда:**
|
||||
|
||||
| Стенд | URL |
|
||||
|---|---|
|
||||
| Dev | `https://auth-api-dev.ngcloud.ru` |
|
||||
| Test | `https://auth-api-test.ngcloud.ru` |
|
||||
| Prod | `https://auth-api.ngcloud.ru` |
|
||||
|
||||
**Swagger-документация:** `https://auth-api-dev.ngcloud.ru/api/v1/documentation/`
|
||||
|
||||
---
|
||||
|
||||
## 2. Исходные данные (Elma CRM)
|
||||
|
||||
Изначальный источник — CRM Elma. Две сущности:
|
||||
|
||||
- **Контакт** (contact) — человек, пользователь
|
||||
- **Компания** (company) — организация
|
||||
|
||||
Отношение **не 1:1**. Контакт может представлять несколько компаний. Это нормальная ситуация.
|
||||
|
||||
В токене IAM, который пользователь получает в ЛК, зашита **основная** компания. Но через API IAM можно получить **все** доступные пользователю компании и узнать, какая из них **активна в данный момент**.
|
||||
|
||||
Смежные сервисы ходят в IAM API для проверки, что услуга выпускается для правильной компании.
|
||||
|
||||
---
|
||||
|
||||
## 3. Ключевые эндпоинты IAM API
|
||||
|
||||
### 3.1. `GET /api/v1/auth/user` — информация о пользователе
|
||||
|
||||
**Назначение:** получение полной информации о текущем пользователе, включая список всех доступных профилей (компаний) и флаг активного профиля.
|
||||
|
||||
**Авторизация:** `Authorization: Bearer <token>`
|
||||
|
||||
**Реальный ответ** (тестовый пользователь `tazetdinovn@gmail.com`, компания `naeel_test`, ClientID `WZ03709`):
|
||||
|
||||
```json
|
||||
{
|
||||
"impersonation": {
|
||||
"is_impersonated": false
|
||||
},
|
||||
"isPortal": false,
|
||||
"needChangePassword": false,
|
||||
"owner": false,
|
||||
"permissions": {
|
||||
"can_write": false,
|
||||
"has_write_permissions": false,
|
||||
"is_impersonating": false,
|
||||
"read_only_mode_enabled": false,
|
||||
"reason": "insufficient_permissions"
|
||||
},
|
||||
"privileges": null,
|
||||
"profiles": [
|
||||
{
|
||||
"id": 2833,
|
||||
"client_id": "WZ03709",
|
||||
"company_id": "019cc24a-727e-740f-b407-bec79dab4162",
|
||||
"company_name": "naeel_test",
|
||||
"company_numeric_id": 2645,
|
||||
"is_active_profile": true
|
||||
}
|
||||
],
|
||||
"roles": [
|
||||
{
|
||||
"created_at": "0001-01-01T00:00:00Z",
|
||||
"role_id": 1,
|
||||
"role_name": "Пользователь",
|
||||
"user_uuid": "019cc268-6c6a-781e-8613-4bed4ec7cd20"
|
||||
}
|
||||
],
|
||||
"sessionId": "",
|
||||
"userId": "019cc268-6c6a-781e-8613-4bed4ec7cd20",
|
||||
"userInfo": {
|
||||
"accounts": [],
|
||||
"avatar": [""],
|
||||
"clientID": "WZ03709",
|
||||
"company": "naeel_test",
|
||||
"companyId": "019cc24a-727e-740f-b407-bec79dab4162",
|
||||
"companyManager": "",
|
||||
"companyNumericId": 2645,
|
||||
"contactId": "019cc268-6c6a-781e-8613-4bed4ec7cd20",
|
||||
"currentSupportLevel": "",
|
||||
"elmaUserId": 17193,
|
||||
"email": "tazetdinovn@gmail.com",
|
||||
"externalUser": true,
|
||||
"fio": {
|
||||
"fullName": "Тазетдинов Наиль Фаритович",
|
||||
"name": "Наиль",
|
||||
"secondName": "Фаритович",
|
||||
"surname": "Тазетдинов"
|
||||
},
|
||||
"groupIds": null,
|
||||
"integration": { "serviceId": "" },
|
||||
"isAdmin": false,
|
||||
"login": "",
|
||||
"mobilePhone": [],
|
||||
"position": "",
|
||||
"userId": "019cc268-6c6a-781e-8613-4bed4ec7cd20"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Ключевые поля для интеграции:
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| `profiles[]` | array | **Все** доступные пользователю профили (компании) |
|
||||
| `profiles[].id` | integer | ID профиля для `POST /switch-profile` |
|
||||
| `profiles[].client_id` | string | WZ-код компании |
|
||||
| `profiles[].company_id` | string | UUID компании |
|
||||
| `profiles[].company_name` | string | Название компании |
|
||||
| `profiles[].company_numeric_id` | integer | Числовой ID компании |
|
||||
| `profiles[].is_active_profile` | boolean | **Какой профиль активен в данный момент** |
|
||||
| `userInfo.clientID` | string | WZ-код **активной** компании |
|
||||
| `userInfo.company` | string | Название активной компании |
|
||||
| `userInfo.companyId` | string | UUID активной компании |
|
||||
| `userInfo.companyNumericId` | integer | Числовой ID активной компании |
|
||||
| `userInfo.email` | string | Email пользователя |
|
||||
| `userInfo.isAdmin` | boolean | Флаг администратора |
|
||||
| `userInfo.fio` | object | ФИО пользователя |
|
||||
| `userInfo.contactId` | string | UUID контакта в Elma |
|
||||
| `userId` | string | UUID пользователя |
|
||||
|
||||
#### Пример для пользователя с несколькими компаниями:
|
||||
|
||||
```json
|
||||
"profiles": [
|
||||
{
|
||||
"id": 2833,
|
||||
"client_id": "WZ03709",
|
||||
"company_name": "naeel_test",
|
||||
"is_active_profile": true // ← активная
|
||||
},
|
||||
{
|
||||
"id": 3120,
|
||||
"client_id": "WZ12345",
|
||||
"company_name": "Другая Компания",
|
||||
"is_active_profile": false // ← неактивная
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 3.2. `POST /api/v1/user/switch-profile` — переключение компании
|
||||
|
||||
**Назначение:** переключить активный профиль пользователя на другую компанию.
|
||||
|
||||
**Тело запроса:**
|
||||
```json
|
||||
{
|
||||
"profile_id": 3120
|
||||
}
|
||||
```
|
||||
или
|
||||
```json
|
||||
{
|
||||
"company_id": "uuid-компании"
|
||||
}
|
||||
```
|
||||
|
||||
**Ответ:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"active_profile": {
|
||||
"id": 3120,
|
||||
"company_id": "uuid",
|
||||
"company_name": "Другая Компания",
|
||||
"email": "user@example.com",
|
||||
"full_name": "ФИО",
|
||||
"contact_id": "uuid"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Текущее состояние ipwhitelist-app
|
||||
|
||||
### 4.1. Как работает сейчас
|
||||
|
||||
Приложение **НИ РАЗУ не вызывает IAM API**. Вся информация о пользователе и компаниях извлекается из JWT-токена в `src/auth.js`:
|
||||
|
||||
```javascript
|
||||
// src/auth.js — userFromPayload()
|
||||
const rawClientId = payload.ClientID || payload.client_id || '';
|
||||
const allClientIds = rawClientId.split(',').map(s => s.trim()).filter(Boolean);
|
||||
// + поиск WZ* в claims массиве
|
||||
|
||||
return {
|
||||
clientId: activeClientId, // первый из allClientIds
|
||||
allClientIds, // comma-separated из ClientID
|
||||
activeClientId, // то же что и clientId
|
||||
companyId: payload.company_id,
|
||||
companyName: payload.company_name,
|
||||
isAdmin: activeClientId === 'WZ01112',
|
||||
};
|
||||
```
|
||||
|
||||
### 4.2. Проблемы
|
||||
|
||||
1. **`allClientIds` из токена — ненадёжно.** JWT содержит только `ClientID` активной компании (одно значение). Comma-separated значения — хак/предположение, не гарантированное IAM.
|
||||
|
||||
2. **Нет запроса к IAM API.** Приложение должно делать `GET /api/v1/auth/user` для получения актуального списка профилей.
|
||||
|
||||
3. **`is_active_profile` не используется.** Приложение понятия не имеет, какой профиль активен с точки зрения IAM. Активная компания определяется как «первая в списке».
|
||||
|
||||
4. **Переключение компаний — только локальное.** В `ui/routes/entries.js` параметр `?switchTo=` меняет `activeClientId` в сессии, но не вызывает `POST /switch-profile` на IAM. Если пользователь переключит компанию через ЛК, приложение об этом не узнает.
|
||||
|
||||
5. **Проверка `isAdmin` — хардкод.** `activeClientId === 'WZ01112'` вместо использования `userInfo.isAdmin` из IAM.
|
||||
|
||||
6. **Избыточный парсинг JWT.** Поля `company_id`, `company_name`, `ClientID` парсятся из токена, хотя правильный источник — ответ IAM API.
|
||||
|
||||
### 4.3. Что должно быть
|
||||
|
||||
| Данные | Сейчас (источник) | Должно быть (источник) |
|
||||
|---|---|---|
|
||||
| Список компаний | `ClientID` из JWT | `profiles[].client_id` из IAM |
|
||||
| Активная компания | Первая из списка | `profiles[].is_active_profile === true` |
|
||||
| WZ-код | `payload.ClientID` | `userInfo.clientID` |
|
||||
| Название компании | `payload.company_name` | `userInfo.company` или `profiles` |
|
||||
| UUID компании | `payload.company_id` | `userInfo.companyId` |
|
||||
| isAdmin | `clientId === 'WZ01112'` | `userInfo.isAdmin` |
|
||||
| Переключение | Сессия (`req.session.user.activeClientId`) | `POST /switch-profile` + сессия |
|
||||
|
||||
---
|
||||
|
||||
## 5. План доработок
|
||||
|
||||
### 5.1. `src/config.js` — добавить URL IAM API
|
||||
|
||||
```javascript
|
||||
const IAM_API_URL = process.env.IAM_API_URL || 'https://auth-api-test.ngcloud.ru';
|
||||
```
|
||||
|
||||
### 5.2. `src/auth.js` — добавить `fetchIamUser(token)`
|
||||
|
||||
Новая функция, которая делает HTTP-запрос к `GET {IAM_API_URL}/api/v1/auth/user` и возвращает объект пользователя на основе ответа IAM:
|
||||
|
||||
```javascript
|
||||
async function fetchIamUser(token) {
|
||||
// GET {IAM_API_URL}/api/v1/auth/user
|
||||
// Authorization: Bearer <token>
|
||||
// Возвращает:
|
||||
// {
|
||||
// email: userInfo.email,
|
||||
// clientId: userInfo.clientID, // активная компания
|
||||
// allClientIds: profiles.map(p => p.client_id),
|
||||
// activeProfileId: profiles.find(p => p.is_active_profile)?.id,
|
||||
// companyId: userInfo.companyId,
|
||||
// companyName: userInfo.company,
|
||||
// isAdmin: userInfo.isAdmin,
|
||||
// userId: userId,
|
||||
// contactId: userInfo.contactId,
|
||||
// fio: userInfo.fio,
|
||||
// profiles: profiles, // полный список для UI
|
||||
// }
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3. `src/auth.js` — изменить `userFromPayload()`
|
||||
|
||||
После получения токена (в OIDC-режиме) вызывать `fetchIamUser()` и использовать его результат вместо парсинга JWT.
|
||||
|
||||
В mock-режиме — оставить текущую логику (IAM API недоступен локально).
|
||||
|
||||
### 5.4. `src/auth.js` — добавить `switchProfile(token, profileId)`
|
||||
|
||||
```javascript
|
||||
async function switchProfile(token, profileId) {
|
||||
// POST {IAM_API_URL}/api/v1/user/switch-profile
|
||||
// Body: { profile_id: profileId }
|
||||
// Возвращает новый active_profile
|
||||
}
|
||||
```
|
||||
|
||||
### 5.5. `ui/routes/entries.js` — изменить логику `?switchTo=`
|
||||
|
||||
При переключении компании:
|
||||
1. Вызвать `auth.switchProfile(token, profileId)` на IAM
|
||||
2. Обновить `req.session.user` с новым `activeClientId`
|
||||
3. Сохранить актуальный список `profiles`
|
||||
|
||||
### 5.6. `ui/routes/entries.js` — передавать `profiles` в шаблон
|
||||
|
||||
Для отображения списка доступных компаний в UI (выпадающий список с отметкой активной).
|
||||
|
||||
---
|
||||
|
||||
## 6. Сетевая доступность — ddos-guard и TLS fingerprint
|
||||
|
||||
### 6.1. Проблема
|
||||
|
||||
Все стенды `*.ngcloud.ru` защищены ddos-guard. При попытке доступа через `curl` без флага `--http2` с сервера (IP `81.200.23.210`) — **403 Forbidden** на любых эндпоинтах.
|
||||
|
||||
При этом **браузер с того же самого IP** проходит нормально. То есть ddos-guard блокирует не по IP, а по сочетанию протокола (HTTP/1.1 vs HTTP/2) и TLS-отпечатка клиента.
|
||||
|
||||
### 6.2. Матрица доступности (проверено 2026-06-04)
|
||||
|
||||
| Клиент | auth-api | deck-api-test |
|
||||
|---|---|---|
|
||||
| `curl` (HTTP/1.1, по умолчанию) | ❌ 403 | ❌ 403 |
|
||||
| `curl --http2` | ✅ 200 | ❌ 403 |
|
||||
| `curl --http2` + браузерные заголовки | ✅ 200 | ❌ 403 |
|
||||
| `Node.js https` | ✅ 200 | ✅ 200 |
|
||||
| `Python urllib` | ✅ 200 | Не проверялся |
|
||||
| Браузер (Chrome/Firefox) | ✅ 200 | ✅ 200 |
|
||||
|
||||
**Выводы:**
|
||||
- `auth-api`: проблема curl решается флагом `--http2`
|
||||
- `deck-api-test`: ddos-guard проверяет TLS fingerprint, curl не проходит даже с HTTP/2. Node.js — проходит
|
||||
- **Приложение на Node.js работает везде**
|
||||
|
||||
### 6.3. Как использовать curl (правильно!)
|
||||
|
||||
```bash
|
||||
# ❌ НЕ РАБОТАЕТ — HTTP/1.1 режется ddos-guard
|
||||
curl -H "Authorization: Bearer $TOKEN" https://auth-api.ngcloud.ru/api/v1/auth/user
|
||||
|
||||
# ✅ РАБОТАЕТ — HTTP/2 + браузерные заголовки
|
||||
curl --http2 \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Accept: application/json" \
|
||||
-H "Accept-Language: ru-RU,ru;q=0.9" \
|
||||
"https://auth-api.ngcloud.ru/api/v1/auth/user"
|
||||
|
||||
# ✅ РАБОТАЕТ — Node.js (всегда)
|
||||
node -e "
|
||||
const https = require('https');
|
||||
https.get({
|
||||
hostname: 'auth-api.ngcloud.ru',
|
||||
path: '/api/v1/auth/user',
|
||||
headers: { Authorization: 'Bearer $TOKEN', Accept: 'application/json' }
|
||||
}, res => { let d=''; res.on('data',c=>d+=c); res.on('end',()=>console.log(d)); });
|
||||
"
|
||||
|
||||
# ✅ РАБОТАЕТ — Python
|
||||
python3 -c "
|
||||
import urllib.request
|
||||
req = urllib.request.Request('https://auth-api.ngcloud.ru/api/v1/auth/user',
|
||||
headers={'Authorization': 'Bearer $TOKEN', 'Accept': 'application/json'})
|
||||
print(urllib.request.urlopen(req, timeout=10).read().decode())
|
||||
"
|
||||
```
|
||||
|
||||
### 6.4. Значение для приложения
|
||||
|
||||
Приложение **ipwhitelist-app** написано на Node.js — оно **может** ходить в IAM API с этого сервера без проблем. `curl` без `--http2` врал про отсутствие доступа.
|
||||
|
||||
---
|
||||
|
||||
## 7. Проверка токенов (подтверждено)
|
||||
|
||||
### 7.1. Типы токенов
|
||||
|
||||
| Тип | `token_type` | TTL | Где взять |
|
||||
|---|---|---|---|
|
||||
| **access** | `access` | 12 часов | Ответ IAM после SSO через Keycloak (`POST /token`) |
|
||||
| **refresh** | `refresh` | 7 дней | Там же, для обновления access |
|
||||
| **tech** | `tech` | 6 месяцев | ЛК → вкладка «Токены» → «Выпустить тех-токен» |
|
||||
|
||||
### 7.2. Все токены подходят для `GET /api/v1/auth/user`
|
||||
|
||||
Не важно, access или tech — любой валидный IAM-токен (`iss: "auth-api"`) принимается. Источник токена (ЛК, SSO через Keycloak, другое приложение) значения не имеет.
|
||||
|
||||
### 7.3. Проверка access_token из ЛК (2026-06-04)
|
||||
|
||||
```bash
|
||||
# Токен, полученный через F12 → Network → /token response → access_token
|
||||
node -e "const https=require('https');
|
||||
https.get({hostname:'auth-api.ngcloud.ru', path:'/api/v1/auth/user',
|
||||
headers:{Authorization:'Bearer $ACCESS_TOKEN', Accept:'application/json'}},
|
||||
res=>{let d=''; res.on('data',c=>d+=c); res.on('end',()=>console.log(d))});"
|
||||
# → HTTP 200 + JSON с profiles[] и userInfo
|
||||
```
|
||||
|
||||
### 7.4. Тестовый пользователь
|
||||
|
||||
| Поле | Test | Prod |
|
||||
|---|---|---|
|
||||
| `email` | `tazetdinovn@gmail.com` | `tazetdinovn@gmail.com` |
|
||||
| `clientID` | `WZ03709` | `WZ03709` |
|
||||
| `company_name` | `naeel_test` | `naeel_test` |
|
||||
| `company_numeric_id` | 2645 | 2645 |
|
||||
| `profile.id` | 2833 | 4357 |
|
||||
| `elmaUserId` | 17193 | 39715 |
|
||||
| `isAdmin` | false | false |
|
||||
| `is_active_profile` | true | true |
|
||||
| Количество профилей | 1 | 1 |
|
||||
|
||||
**Внимание:** у этого пользователя один профиль. Реальная картина с несколькими компаниями будет содержать несколько объектов в `profiles[]`.
|
||||
|
||||
### 7.5. Поток аутентификации (подтверждён)
|
||||
|
||||
```
|
||||
Пользователь → Keycloak (SSO) → IAM (обмен code) → access_token
|
||||
│
|
||||
┌───────────────────────────────┘
|
||||
▼
|
||||
GET /api/v1/auth/user (Bearer access_token)
|
||||
│
|
||||
▼
|
||||
{ profiles[], userInfo }
|
||||
│
|
||||
▼
|
||||
Строим session.user
|
||||
```
|
||||
|
||||
**Никакого дополнительного обмена токенами не требуется.** `access_token` от IAM сразу годится для вызова `GET /api/v1/auth/user`.
|
||||
|
||||
### 6.3. Токен
|
||||
|
||||
Использованный для тестирования токен:
|
||||
- **Тип:** tech-токен (долгоживущий, 6 месяцев)
|
||||
- **Issuer:** `auth-api`
|
||||
- **Пользователь:** `tazetdinovn@gmail.com`
|
||||
- **ClientID:** `WZ03709`
|
||||
- **Компания:** `naeel_test` (numeric_id: 2645)
|
||||
- **Выпущен:** 31 мая 2026
|
||||
- **Истекает:** 30 ноября 2026
|
||||
- **Роль:** Пользователь (не админ)
|
||||
- **Стенд:** test (`auth-api-test.ngcloud.ru`)
|
||||
|
||||
---
|
||||
|
||||
## 7. Файлы, которые нужно изменить
|
||||
|
||||
| Файл | Что изменить |
|
||||
|---|---|
|
||||
| `.env.example` | Добавить `IAM_API_URL` |
|
||||
| `src/config.js` | Добавить константу `IAM_API_URL` из env |
|
||||
| `src/auth.js` | Добавить `fetchIamUser()`, `switchProfile()`, изменить `userFromPayload()` |
|
||||
| `ui/routes/auth.js` | После логина вызывать `fetchIamUser` для обогащения user |
|
||||
| `src/routes/oidc.js` | После callback вызывать `fetchIamUser` |
|
||||
| `ui/routes/entries.js` | `?switchTo=` → вызывать IAM `switchProfile`, передавать `profiles` в шаблон |
|
||||
| `views/index.ejs` | Отображать список компаний с активной |
|
||||
| `src/api/routes/entries.js` | `resolveCompany()` — использовать `profiles` вместо `allClientIds` из токена |
|
||||
|
||||
---
|
||||
|
||||
## 8. Диаграмма целевого потока
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant B as Браузер
|
||||
participant A as ipwhitelist-app
|
||||
participant KC as Keycloak
|
||||
participant IAM as IAM API
|
||||
|
||||
B->>A: Заход на /
|
||||
A->>KC: Редирект на /auth
|
||||
B->>KC: Вход (SSO если есть сессия)
|
||||
KC->>B: Редирект с code
|
||||
B->>A: /callback?code=xxx
|
||||
A->>KC: POST /token (обмен code)
|
||||
KC-->>A: access_token
|
||||
A->>IAM: GET /auth/user (Bearer access_token)
|
||||
IAM-->>A: { profiles[], userInfo }
|
||||
A->>A: Строим user из IAM ответа
|
||||
A->>B: Сессия создана, добро пожаловать
|
||||
|
||||
Note over B,A: Пользователь переключает компанию
|
||||
|
||||
B->>A: /?switchTo=WZ12345
|
||||
A->>IAM: POST /user/switch-profile { profile_id: 3120 }
|
||||
IAM-->>A: { success: true, active_profile }
|
||||
A->>A: Обновляем сессию
|
||||
A->>B: Страница с новой активной компанией
|
||||
```
|
||||
@@ -0,0 +1,184 @@
|
||||
# Keycloak Auth Reference — консолидированная справка
|
||||
|
||||
> Обновлено: 2026-06-02
|
||||
> Файл-ориентир. Править по мере уточнения у DevOps и продвижения проекта.
|
||||
|
||||
---
|
||||
|
||||
## 1. Keycloak-эндпоинты
|
||||
|
||||
Realm: **`cloud`** на `keycloak.nubes.ru`
|
||||
|
||||
| Назначение | URL |
|
||||
|---|---|
|
||||
| Authorization | `https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/auth` |
|
||||
| Token exchange | `https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/token` |
|
||||
| JWKS (сертификаты) | `https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/certs` |
|
||||
| Logout | `https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/logout` |
|
||||
|
||||
---
|
||||
|
||||
## 2. Зарегистрированное приложение (OAuth client)
|
||||
|
||||
`client_id` — выдаётся DevOps при регистрации приложения в Keycloak.
|
||||
|
||||
---
|
||||
|
||||
## 3. Схема аутентификации: Authorization Code Flow
|
||||
|
||||
```
|
||||
1. Браузер → /login (приложение)
|
||||
2. Приложение → 302 → Keycloak /auth?client_id=...&scope=openid&redirect_uri=...&state=...
|
||||
3. Пользователь вводит логин/пароль в Keycloak
|
||||
4. Пользователь вводит OTP (MFA)
|
||||
5. Keycloak → 302 → /callback?code=...&state=...
|
||||
6. Приложение → POST /token (code → access_token + refresh_token + id_token)
|
||||
7. Валидация JWT через JWKS (RS256)
|
||||
8. Извлечение claims → req.user → сессия
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Получение токена через API Gateway (альтернативный путь)
|
||||
|
||||
Если в инфраструктуре есть API Gateway, код обменивается не напрямую в Keycloak, а через него:
|
||||
|
||||
```
|
||||
POST <url API Gateway>/token
|
||||
Content-Type: application/json
|
||||
Body: {"code": "<authorization_code>"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. JWT-токены
|
||||
|
||||
### 5.1. Пример структуры JWT (deprecated)
|
||||
|
||||
```
|
||||
iss: https://<issuer>/...
|
||||
aud: <audience>
|
||||
sub: <email>
|
||||
name: <ФИО пользователя>
|
||||
email: <email пользователя>
|
||||
exp: <timestamp>
|
||||
scope: openid
|
||||
token_type: Bearer
|
||||
```
|
||||
|
||||
> ⚠️ `ClientID` может отсутствовать — зависит от issuer'а.
|
||||
|
||||
### 5.2. Пример токена
|
||||
|
||||
| Поле | Значение |
|
||||
|---|---|
|
||||
| `iss` | `auth-api` |
|
||||
| `ClientID` | `WZ01325` |
|
||||
| `company_id` | `<uuid>` |
|
||||
| `company_name` | Название компании |
|
||||
| `login` | `user@example.com` |
|
||||
| `email` | `user@example.com` |
|
||||
|
||||
### 5.3. Целевой формат токена (из ТЗ)
|
||||
|
||||
| Claim | Назначение |
|
||||
|---|---|
|
||||
| `ClientID` | Идентификатор компании (WZ-номер) |
|
||||
| `company_id` | UUID компании |
|
||||
| `company_name` | Название компании |
|
||||
| `email` | Email пользователя (для аудита) |
|
||||
| `groups` | ? (пока null) |
|
||||
| `roles` | ? (пока null) |
|
||||
|
||||
---
|
||||
|
||||
## 6. Роли
|
||||
|
||||
| Роль | ClientID | Права |
|
||||
|---|---|---|
|
||||
| **Администратор** | `WZ01112` | Все записи, просмотр аудита, экспорт |
|
||||
| **Клиент** | любой другой WZ-номер | Только свои записи |
|
||||
|
||||
> Как именно ClientID попадает в токен — через группы Keycloak или маппер — **требует уточнения у DevOps**.
|
||||
|
||||
---
|
||||
|
||||
## 7. Реализация в коде
|
||||
|
||||
### `src/auth.js` — два режима
|
||||
|
||||
| Режим | Условие | Как работает |
|
||||
|---|---|---|
|
||||
| **OIDC** | `KC_CLIENT_ID` + `KC_CLIENT_SECRET` заданы | Authorization Code Flow, JWKS-валидация |
|
||||
| **Mock** | `KC_CLIENT_ID` пусто | Локальная RSA-пара, JWT из `MOCK_USERS` |
|
||||
|
||||
### `src/config.js` — Mock-пользователи
|
||||
|
||||
```javascript
|
||||
MOCK_USERS = [
|
||||
{ clientId: 'WZ01325', companyName: 'Тест', email: 'test@example.com' },
|
||||
{ clientId: 'WZ01112', companyName: 'Нубес', email: 'admin@nubes.ru' }
|
||||
]
|
||||
```
|
||||
|
||||
### Два слоя аутентификации
|
||||
|
||||
| Слой | Механизм | Где |
|
||||
|---|---|---|
|
||||
| **UI (EJS)** | Session cookie + PostgreSQL session store | `/`, `/admin`, `/entries` |
|
||||
| **REST API** | Bearer JWT (всегда проверяется явно) | `/api/v1/*` |
|
||||
|
||||
### Dev-backdoor
|
||||
|
||||
- `DEV_MODE=true` или `DEV_SECRET` → `/dev-login` — выбор любого пользователя в обход OIDC/mock
|
||||
|
||||
---
|
||||
|
||||
## 8. Переменные окружения
|
||||
|
||||
```env
|
||||
# Auth
|
||||
DEV_MODE=true
|
||||
JWT_ISSUER=mock-auth-api
|
||||
ADMIN_CLIENT_ID=WZ01112
|
||||
|
||||
# OIDC (production)
|
||||
KC_BASE_URL=https://keycloak.nubes.ru/realms/cloud
|
||||
KC_CLIENT_ID=
|
||||
KC_CLIENT_SECRET=
|
||||
APP_URL=http://localhost:3000
|
||||
|
||||
# Session
|
||||
SESSION_SECRET=<обязателен в prod>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Открытые вопросы (к DevOps)
|
||||
|
||||
| # | Вопрос | Статус |
|
||||
|---|---|---|
|
||||
| 1 | Где брать `client_id` и `client_secret` для регистрации приложения в Keycloak? | 🔴 |
|
||||
| 2 | Какой URL JWKS? `auth-api` или `keycloak.nubes.ru`? | 🔴 |
|
||||
| 3 | Когда заполняются `claims.groups` и `claims.roles`? (пока null) | 🔴 |
|
||||
| 4 | Как выглядит токен, если у пользователя несколько компаний? Массив `ClientID`? | 🔴 |
|
||||
| 5 | Есть ли в Keycloak понятие admin-роли? В каком claim? | 🔴 |
|
||||
| 6 | Redirect URI: `https://white.nodejsk8s.dev.nubes.ru/callback` — OK? | 🔴 |
|
||||
| 7 | Кто потребитель `/export`? Какие IP/подсети для allowlist? | 🔴 |
|
||||
| 8 | Формат `jsonEnv` — пример правильного? | 🔴 |
|
||||
|
||||
---
|
||||
|
||||
## 10. Исходные файлы
|
||||
|
||||
| Файл | Содержание |
|
||||
|---|---|
|
||||
| `docs/ТЗ.md` | Техническое задание |
|
||||
| `docs/ТЗ-реализация.md` | Карта реализации требований |
|
||||
| `docs/questions-devops.md` | Вопросы к DevOps |
|
||||
| `research/auth-flow.md` | Детальное исследование OIDC-flow |
|
||||
| `research/payg-sso/` | CF-эталон (OAuth2 + JWT RS256) |
|
||||
| `Files/token.txt` | Реальный токен из test-окружения |
|
||||
| `Files/nubes_login.har` | HAR трассировка login-сессии deck-test |
|
||||
| `src/auth.js` | Основной модуль аутентификации |
|
||||
| `src/config.js` | Конфигурация и mock-пользователи |
|
||||
@@ -0,0 +1,161 @@
|
||||
# ТЗ-плюс — уточнения и дополнения от заказчика
|
||||
|
||||
> Основа: `docs/ТЗ.md`
|
||||
> Файл для фиксации уточнений, дополнений и решений по мере обсуждения с заказчиком/DevOps.
|
||||
> Каждая запись = дата + источник + формулировка + статус.
|
||||
|
||||
---
|
||||
|
||||
## 1. Аутентификация и Keycloak
|
||||
|
||||
### 1.1. client_id приложения в Keycloak
|
||||
- **Дата**: 2026-06-02
|
||||
- **Источник**: HAR-файл
|
||||
- **Факт**: `client_id` присутствует в URL auth-запроса
|
||||
- **Статус**: 🔴 требует подтверждения — какой client_id будет для ipwhitelist?
|
||||
|
||||
### 1.2. ClientID в JWT — УТОЧНЕНО
|
||||
- **ТЗ**: claim `ClientID` — идентификатор компании
|
||||
- **Факт (HAR)**: `ClientID: "WZ01325"`
|
||||
- **Факт (другой источник)**: `ClientID` может отсутствовать
|
||||
- **Уточнение заказчика (01.06.2026)**: в claims приходит `clientid`. Формат может быть разный.
|
||||
- Одно значение: `WZ04228`, `1700`, `asokolov-test`
|
||||
- Несколько через запятую: `WZ11125, WZ03816`, `WZ52235, WZ62587, WZ02315`
|
||||
- **Логика**: первое «слово» до запятой определяет общий ЛК.
|
||||
Пользователи с одинаковым первым словом → общий whitelist.
|
||||
- **Что делать в коде**: брать первый `clientid` до запятой как активную компанию.
|
||||
Остальные (если есть) — дополнительные компании пользователя для переключателя.
|
||||
- **Статус**: 🟡 уточнено, требует реализации в коде
|
||||
|
||||
### 1.3. Несколько компаний на пользователя — УТОЧНЕНО
|
||||
- **ТЗ, п.3.1**: «поддерживается сценарий, когда пользователь принадлежит нескольким компаниям»
|
||||
- **Уточнение заказчика (01.06.2026)**: поле `clientid` в claims содержит список через запятую.
|
||||
Примеры: `WZ11125, WZ03816`, `WZ52235, WZ62587, WZ02315`.
|
||||
- **Логика ЛК**: первое значение до запятой — активная компания.
|
||||
Пользователи с одинаковым первым `clientid` видят общий whitelist.
|
||||
Переключатель между компаниями — все значения из списка.
|
||||
- **Открытый вопрос**: какое именно поле в Keycloak за это отвечает? Заказчик уточнит.
|
||||
- **Статус**: 🟡 уточнено, требует реализации в коде
|
||||
|
||||
### 1.4. Admin-роль в Keycloak
|
||||
- **ТЗ**: admin = `ClientID = WZ01112` + отдельный чек-бокс
|
||||
- **Вопрос**: чек-бокс — это claim? Какой? `is_admin`, `realm_access.roles`, `groups`?
|
||||
- **Статус**: 🔴
|
||||
|
||||
### 1.5. JWKS URL
|
||||
- **Вариант A**: `https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/certs`
|
||||
- **Вариант B**: `https://auth-api...` (через API Gateway)
|
||||
- **Статус**: 🔴
|
||||
|
||||
### 1.6. Redirect URI
|
||||
- **Предложение**: `https://<домен>/callback`
|
||||
- **Статус**: 🔴 требует подтверждения DevOps
|
||||
|
||||
---
|
||||
|
||||
## 2. Инфраструктура и деплой
|
||||
|
||||
### 2.1. Платформа
|
||||
- **Статус**: 🔴 уточнить — Node.js на k8s? Какой кластер?
|
||||
|
||||
### 2.2. Деплой
|
||||
- **Статус**: 🔴 уточнить — CI/CD? Как поставлять `jsonEnv`?
|
||||
|
||||
### 2.3. Сетевое ограничение для /export
|
||||
- **Вопрос**: какие IP/подсети будут потребителями внешней выдачи?
|
||||
- **Статус**: 🔴
|
||||
|
||||
---
|
||||
|
||||
## 3. Функциональные уточнения
|
||||
|
||||
### 3.1. Мульти-компания в UI
|
||||
- **ТЗ, п.3.1**: «переключатель активной компании»
|
||||
- **Вопрос**: дизайн переключателя? Dropdown в шапке? Отдельная страница?
|
||||
- **Статус**: 🔴
|
||||
|
||||
### 3.2. Soft-delete и фильтр «показать удалённые»
|
||||
- **ТЗ, п.4.1**: «для администратора предусмотрен фильтр»
|
||||
- **Код**: `includeDeleted` всегда `false`
|
||||
- **Статус**: 🟡 не реализовано в коде
|
||||
|
||||
### 3.3. Индивидуальные лимиты компаний
|
||||
- **ТЗ, п.4.5**: админ может задать индивидуальный лимит
|
||||
- **Код**: ?
|
||||
- **Статус**: 🔴 проверить реализацию
|
||||
|
||||
### 3.4. Уведомление о нормализации
|
||||
- **ТЗ, п.5**: «пользователь должен быть уведомлен, что ввел адрес из хостовой части»
|
||||
- **Статус**: 🔴 проверить реализацию в UI
|
||||
|
||||
---
|
||||
|
||||
## 4. Тестовые данные
|
||||
|
||||
### 4.1. Тестовые пользователи
|
||||
|
||||
| Email | ClientID | Компания | Роль |
|
||||
|---|---|---|---|
|
||||
| `client@example.com` | `WZ01325` | Тест | Клиент |
|
||||
| `admin@nubes.ru` | `WZ01112` | Нубес | Админ |
|
||||
| `tazetdinovn@gmail.com` | ? | ? | ? (из token.txt) |
|
||||
|
||||
### 4.2. HAR-сессия
|
||||
- Файл: `Files/nubes_login.har`
|
||||
- Пользователь: `tazet@narod.ru`
|
||||
- Дата: 2026-05-30
|
||||
- Окружение: deck-test
|
||||
|
||||
---
|
||||
|
||||
## 6. Уточнения от заказчика (01.06.2026)
|
||||
|
||||
### 6.1. Формат clientid в токене
|
||||
- Поле: `clientid` в claims JWT
|
||||
- Формат: строка, значения через запятую если несколько
|
||||
- Примеры:
|
||||
- `"WZ04228"` — одна компания
|
||||
- `"1700"` — числовой ID
|
||||
- `"asokolov-test"` — текстовый ID
|
||||
- `"WZ11125, WZ03816"` — две компании
|
||||
- `"WZ52235, WZ62587, WZ02315"` — три компании
|
||||
- **Правило**: первое «слово» до запятой = активная компания (определяет ЛК)
|
||||
- **Общий доступ**: пользователи с одинаковым первым словом имеют общий whitelist
|
||||
|
||||
### 6.2. Экспорт — подтверждение формата
|
||||
- GET-endpoint отдаёт txt файл
|
||||
- Одна строка = один объект (адрес/подсеть)
|
||||
- Без разделителей, без заголовков
|
||||
|
||||
### 6.3. Текущее демо — одобрено
|
||||
- `https://white.nodejsk8s.dev.nubes.ru/login` — ✓
|
||||
- `https://white.nodejsk8s.dev.nubes.ru/exp` — ✓ (вывод без токена для тестирования)
|
||||
|
||||
### 6.4. Что ещё уточняется
|
||||
- Какое именно поле Keycloak маппится в `clientid` claim? (Заказчик уточнит)
|
||||
- Как определяется admin-роль? (отдельный чек-бокс в Keycloak?)
|
||||
- `client_id` и `client_secret` для регистрации приложения
|
||||
|
||||
### 6.5. Права внутри компании — уточнено (02.06.2026)
|
||||
- **Вопрос**: может ли любой юзер компании редактировать whitelist всей компании?
|
||||
- **Ответ заказчика**: «Да, любой юзер, который может войти может редактировать»
|
||||
- **Вывод**: внутри компании роли не разграничиваются. Любой сотрудник компании имеет полный доступ к whitelist своей компании (создание, редактирование, удаление). Аудит фиксирует кто именно сделал изменение.
|
||||
|
||||
---
|
||||
|
||||
## 5. Решения и договорённости
|
||||
|
||||
> Сюда записывать принятые решения с датой и контекстом.
|
||||
|
||||
(пока пусто)
|
||||
|
||||
---
|
||||
|
||||
## Легенда статусов
|
||||
|
||||
| Статус | Значение |
|
||||
|---|---|
|
||||
| 🔴 | Требует уточнения |
|
||||
| 🟡 | Уточнено, не реализовано |
|
||||
| 🟢 | Реализовано / подтверждено |
|
||||
| ⚫ | Отменено / не актуально |
|
||||
@@ -0,0 +1,242 @@
|
||||
Техническое задание
|
||||
Микросервис управления доверенными адресами клиентов
|
||||
Self-service портал для указания клиентами доверенных IPv4-адресов и подсетей,
|
||||
исключаемых из блокировки на стороне облачного провайдера во время DDoS-атак
|
||||
|
||||
# 1. Назначение и цели
|
||||
|
||||
## 1.1. Назначение
|
||||
|
||||
Микросервис предоставляет клиентам облачного провайдера web-интерфейс для самостоятельного управления списком доверенных IPv4-адресов и подсетей. Записи из этого списка исключаются из автоматической блокировки сетевого взаимодействия системами фильтрации и митигации провайдера, что снижает количество ложноположительных срабатываний для легитимного трафика клиента.
|
||||
|
||||
> **Реализация:** `server.js` — точка входа, подключает все роуты. `ui/index.js` — UI-слой.
|
||||
|
||||
## 1.2. Цели
|
||||
|
||||
- Дать клиентам возможность самостоятельно поддерживать актуальный список доверенных IPv4-адресов, которые будут исключаться из фильтрации во время DDoS-атак.
|
||||
|
||||
> **Реализация:** `ui/routes/entries.js` — UI CRUD. `src/api/routes/entries.js` — REST API.
|
||||
|
||||
- Предоставить сетевым инженерам единую точку просмотра и управления списками доверенных клиентских белых IPv4-адресов.
|
||||
|
||||
> **Реализация:** `ui/routes/admin.js` — /admin и /audit UI. `src/api/routes/admin.js` — API /companies, /audit.
|
||||
|
||||
- Обеспечить машиночитаемую выдачу агрегированного (суммаризированного) списка для систем фильтрации трафика.
|
||||
|
||||
> **Реализация:** `src/routes/export.js` — GET /export. `src/validators.js` строки 125–181 — функция `aggregateCIDRs`.
|
||||
|
||||
# 2. Объем работ
|
||||
|
||||
- Web-страница / закладка в личном кабинете для управления whitelist-записями.
|
||||
|
||||
> **Реализация:** `views/index.ejs` — главная страница. `ui/routes/entries.js` — роуты GET /, POST /add, POST /edit/:id, POST /delete/:id.
|
||||
|
||||
- Авторизация через существующий экземпляр Keycloak (OIDC).
|
||||
|
||||
> **Реализация:** `src/auth.js` строки 220–260 — верификация JWT RS256 токена Keycloak. Строка 245: чтение claim `ClientID`. Строка 251: определение роли `isAdmin`.
|
||||
|
||||
- Валидация формы на стороне клиента и сервера.
|
||||
|
||||
> **Реализация:** `src/validators.js` строки 26–79 — серверная валидация `validate()`. `views/index.ejs` — атрибут `pattern` в поле ввода (клиентская).
|
||||
|
||||
- Внешний endpoint выдачи агрегированного списка. Выдача txt-файлом с переносом строки. Одна строка – один объект.
|
||||
|
||||
> **Реализация:** `src/routes/export.js` строки 27–64 — GET /export, Content-Type: text/plain, aggregateCIDRs → join('\n').
|
||||
|
||||
- Хранение записей, журнал аудита.
|
||||
|
||||
> **Реализация:** `sql/schema.sql` — таблицы `whitelist_entries`, `audit_log`. `src/queries.js` строки 211–230 — функция `logAudit`.
|
||||
|
||||
- Административное управление лимитами по компаниям.
|
||||
|
||||
> **Реализация:** `src/api/routes/admin.js` строки 27–35 — PATCH /companies/:id/limit. `src/queries.js` — функция `setLimit`. `views/admin.ejs` — UI формы лимитов.
|
||||
|
||||
# 3. Роли и права доступа
|
||||
|
||||
Роли определяются на основании claims в OIDC-токене Keycloak. Соответствие claim → роль настраивается на этапе развёртывания.
|
||||
|
||||
> **Реализация:** `src/auth.js` строка 31: `ADMIN_CLIENT_ID = process.env.ADMIN_CLIENT_ID || 'WZ01112'`. Строка 251: `isAdmin: clientId === ADMIN_CLIENT_ID`.
|
||||
|
||||
| Роль | Идентификация | Видимость записей | Права на изменение |
|
||||
| --- | --- | --- | --- |
|
||||
| Клиент (client) | clientId | Только записи компаний, к которым принадлежит пользователь. | Создание, редактирование и удаление записей своих компаний (в пределах лимита). |
|
||||
| Администратор (admin) | clientId = WZ01112 (Нубес) и отдельный чек-бокс | Записи всех компаний. | Создание, редактирование, удаление всех записей. Изменение лимита для отдельных компаний. |
|
||||
|
||||
> **Реализация ролей в API:** `src/api/routes/entries.js` — функция `resolveCompany()`: если `req.user.isAdmin && req.query.company` → берёт чужую компанию, иначе `getOrCreateCompany(clientId)`. `src/auth.js` строки 260–265 — middleware `requireAdmin`.
|
||||
|
||||
## 3.1. Принадлежность к компании
|
||||
|
||||
Принадлежность пользователя к компании определяется из claim токена. Поддерживается сценарий, когда пользователь принадлежит нескольким компаниям: в этом случае в интерфейсе предусматривается переключатель активной компании, а все операции выполняются в контексте выбранной компании.
|
||||
|
||||
> **Реализация:** `src/auth.js` строка 245: `clientId = payload.ClientID`. `src/queries.js` строки 6–15: `getOrCreateCompany(clientId, companyName)` — атомарный upsert.
|
||||
>
|
||||
> ⚠️ **Сценарий нескольких компаний не реализован** — в реальном токене Nubes `ClientID` = одна строка, массива нет. Ждём уточнения от devops.
|
||||
|
||||
Ожидаемые claims (имена согласуются с командой Keycloak):
|
||||
- `ClientID` — идентификатор компании → `src/auth.js` строка 245
|
||||
- `email` — идентификация пользователя для аудита → `src/auth.js` строка 248
|
||||
|
||||
# 4. Функциональные требования
|
||||
|
||||
## 4.1. Просмотр списка записей
|
||||
|
||||
- Клиент видит таблицу записей активной компании; Администратор – записи всех компаний с фильтром по компании.
|
||||
|
||||
> **Реализация:** `ui/routes/entries.js` — GET /: для admin грузит `getAllCompanies()` + фильтр по `?company=<id>`. `views/index.ejs` — dropdown компаний для admin, таблица записей.
|
||||
|
||||
- Для каждой записи отображаются: значение (адрес/подсеть), комментарий (если есть), автор(email), дата создания, дата последнего изменения.
|
||||
|
||||
> **Реализация:** `views/index.ejs` строки 280–295 — колонки таблицы: `value_cidr`, `comment`, `created_by`, `created_at`, `updated_at`. Даты в timezone `Europe/Moscow`.
|
||||
|
||||
- Soft-deleted записи по умолчанию скрыты; для администратора предусмотрен фильтр для их отображения.
|
||||
|
||||
> **Реализация:** `src/queries.js` строки 25–31 — `listEntries(companyId, includeDeleted)`. SQL: `AND deleted_at IS NULL` когда `includeDeleted=false`.
|
||||
>
|
||||
> ⚠️ **Фильтр "показать удалённые" в UI не реализован** — `includeDeleted` всегда `false`.
|
||||
|
||||
- Отображается текущее использование лимита: «использовано X из N».
|
||||
|
||||
> **Реализация:** `src/api/routes/entries.js` строка 62: `res.json({ entries, limit, used: entries.length })`. `views/index.ejs` — блок статистики `<%= used %> / <%= limit %>`.
|
||||
|
||||
## 4.2. Создание записи
|
||||
|
||||
- Форма содержит поля: значение (IPv4-адрес или подсеть CIDR) и необязательный комментарий (до 255 символов).
|
||||
|
||||
> **Реализация:** `views/index.ejs` — форма POST /add с полями `value` и `comment`. Атрибут `maxlength="18"` на поле адреса.
|
||||
|
||||
- Значение проходит валидацию (см. раздел 5) на клиенте и обязательно повторно на сервере.
|
||||
|
||||
> **Реализация:** `src/validators.js` строки 26–79 — `validate(input)`. Вызывается в `src/queries.js` строка 33: `const { cidr, wasNormalized } = validate(rawValue)`.
|
||||
|
||||
- Перед сохранением проверяется: соблюдение лимита компании, отсутствие пересечений и дубликатов внутри компании, отсутствие принадлежности к запрещённым диапазонам.
|
||||
|
||||
> **Реализация:** `src/queries.js` строки 32–70 — `createEntry()`: блокировка строки компании (FOR UPDATE), проверка лимита (строки 43–46), проверка дубликатов и пересечений (строки 49–57), проверка запрещённых диапазонов в `validate()` (строки 73–75 validators.js).
|
||||
|
||||
- При успешном сохранении создаётся запись аудита.
|
||||
|
||||
> **Реализация:** `src/queries.js` строка 66: `logAudit(userEmail, companyId, 'CREATE', null, cidr, ...)`.
|
||||
|
||||
## 4.3. Редактирование записи
|
||||
|
||||
- Редактирование значения и комментария доступно компании в рамках своих прав.
|
||||
|
||||
> **Реализация:** `ui/routes/entries.js` — POST /edit/:id. `src/api/routes/entries.js` — PATCH /entries/:id. Modal в `views/index.ejs` — кнопка `.btn-edit`, event delegation в `<script>`.
|
||||
|
||||
- При изменении значения повторно выполняется полный набор проверок валидации и пересечений.
|
||||
|
||||
> **Реализация:** `src/queries.js` строки 77–115 — `updateEntry()`: валидация через `validate()`, проверка пересечений (исключая саму запись: `AND id <> $2`).
|
||||
|
||||
- Изменение фиксируется в журнале аудита с сохранением прежнего и нового значения.
|
||||
|
||||
> **Реализация:** `src/queries.js` строка 107: `logAudit(userEmail, companyId, 'UPDATE', old.value_cidr, cidr, entryId, ...)`.
|
||||
|
||||
## 4.4. Удаление записи (soft delete)
|
||||
|
||||
- Удаление выполняется как логическое (soft delete): запись помечается удалённой (deleted_at, deleted_by), но физически сохраняется.
|
||||
|
||||
> **Реализация:** `src/queries.js` строки 118–145 — `deleteEntry()`: UPDATE SET `deleted_at = NOW(), deleted_by = userEmail`. `sql/schema.sql` — колонки `deleted_at`, `deleted_by` в таблице `whitelist_entries`.
|
||||
|
||||
- Удалённая запись освобождает место в лимите компании и исключается из внешней агрегированной выдачи.
|
||||
|
||||
> **Реализация:** `src/queries.js` строка 50: COUNT считает только `WHERE deleted_at IS NULL`. `src/routes/export.js` — запрос только активных записей.
|
||||
|
||||
- Действие фиксируется в журнале аудита.
|
||||
|
||||
> **Реализация:** `src/queries.js` строка 134: `logAudit(userEmail, companyId, 'DELETE', old.value_cidr, null, ...)`.
|
||||
|
||||
## 4.5. Лимит записей на компанию
|
||||
|
||||
- Действует глобальный лимит по умолчанию: 15 активных записей на компанию.
|
||||
|
||||
> **Реализация:** `src/queries.js` строки 17–20 — `getLimit()`: `parseInt(process.env.DEFAULT_LIMIT) || 15`.
|
||||
|
||||
- Значение глобального лимита по умолчанию задаётся конфигурацией сервиса и может быть изменено без пересборки.
|
||||
|
||||
> **Реализация:** `src/queries.js` строка 18: `process.env.DEFAULT_LIMIT` — переменная окружения, не хардкод.
|
||||
|
||||
- Для отдельной компании администратор может задать индивидуальный лимит, переопределяющий глобальный (как в большую, так и в меньшую сторону).
|
||||
|
||||
> **Реализация:** `src/api/routes/admin.js` строки 27–35 — PATCH /companies/:id/limit. `src/queries.js` строка 20: `company.custom_limit != null ? company.custom_limit : defaultLimit`. `sql/schema.sql` — колонка `custom_limit` в таблице `companies`.
|
||||
|
||||
- При попытке превысить лимит создание блокируется с понятным сообщением; в подсчёт идут только активные записи.
|
||||
|
||||
> **Реализация:** `src/queries.js` строки 43–46: `if (cnt >= limit) throw new Error('Лимит исчерпан: N из N')`. API возвращает 409.
|
||||
|
||||
- Снижение лимита ниже текущего числа записей не удаляет существующие записи, но блокирует создание новых до приведения в соответствие.
|
||||
|
||||
> **Реализация:** `src/api/routes/admin.js` строки 28–35 — setLimit просто записывает значение без удаления записей. Блокировка создания — через проверку `cnt >= limit` в `createEntry`.
|
||||
|
||||
## 4.6. Журнал аудита
|
||||
|
||||
- Все изменяющие операции фиксируются неизменяемыми записями аудита.
|
||||
|
||||
> **Реализация:** `src/queries.js` строки 211–218 — `logAudit()`: INSERT в `audit_log` без UPDATE/DELETE операций над ней.
|
||||
|
||||
- Каждая запись аудита содержит: кто (пользователь), когда (timestamp), компания, тип действия, прежнее и новое состояние.
|
||||
|
||||
> **Реализация:** `sql/schema.sql` — таблица `audit_log`: колонки `user_email`, `created_at`, `company_id`, `action`, `old_value`, `new_value`. `views/audit.ejs` строки 131–160 — отображение.
|
||||
|
||||
- Журнал доступен для просмотра только администратору.
|
||||
|
||||
> **Реализация:** `src/api/routes/admin.js` — GET /audit защищён `apiRequireAdmin`. `ui/routes/admin.js` — GET /audit проверяет `req.session.user.isAdmin`.
|
||||
|
||||
## 4.7. Внешняя выдача агрегированного списка
|
||||
|
||||
- Подсети суммаризируются (агрегируются в минимальный набор CIDR) по всем компаниям совместно. Пересечения между разными компаниями допустимы.
|
||||
|
||||
> **Реализация:** `src/validators.js` строки 125–181 — `aggregateCIDRs()`: сортировка, слияние перекрывающихся диапазонов, преобразование обратно в CIDR.
|
||||
|
||||
- Предоставляется отдельный HTTP GET endpoint, отдающий полный суммаризированный список активных записей всех компаний файлом в формате txt.
|
||||
|
||||
> **Реализация:** `src/routes/export.js` строки 27–64 — GET /export. Content-Type: `text/plain`, ответ: `aggregated.join('\n')`.
|
||||
|
||||
- Авторизация: на старте endpoint может работать без авторизации (по сетевому ограничению / разрешенный список потребителей по ip).
|
||||
|
||||
> ⚠️ **Не реализовано** — /export требует Bearer-токен (авторизован). По ТЗ должен быть доступен без авторизации по IP-списку.
|
||||
|
||||
# 5. Требования к валидации
|
||||
|
||||
Валидация выполняется на клиенте и обязательно дублируется на сервере. Серверная валидация является авторитетной.
|
||||
|
||||
> **Реализация:** `src/validators.js` — вся серверная валидация. `views/index.ejs` — атрибут `pattern` (клиент).
|
||||
|
||||
| Правило | Описание | Реализация |
|
||||
| --- | --- | --- |
|
||||
| Формат IPv4 | Допускается одиночный адрес или подсеть CIDR /22–/32. | `src/validators.js` строки 41–53: добавление /32 если нет маски, проверка `mask < 22 \|\| mask > 32`. |
|
||||
| Только IPv4 | IPv6-значения или доменные имена отклоняются. | `src/validators.js` строки 31–36: `if (raw.includes(':'))` → отклонить, проверка букв. |
|
||||
| Корректность подсети | Host-биты обнуляются, пользователь уведомляется о нормализации. | `src/validators.js` строки 55–69: битовая арифметика, `wasNormalized = addr !== networkAddr`. Флаш-сообщение в `ui/routes/entries.js`. |
|
||||
| Запрет серых адресов | Диапазоны из Приложения А запрещены. | `src/validators.js` строки 4–20: `BLOCKED_RANGES[]`. Строки 73–75: проверка `overlaps()`. |
|
||||
| Отсутствие дубликатов | Совпадающие записи в компании запрещены. | `src/queries.js` строка 53: `if (row.value_cidr === cidr) throw`. Уникальный индекс в `sql/schema.sql`. |
|
||||
| Отсутствие пересечений | Пересечение с существующей записью в компании запрещено. | `src/queries.js` строки 49–57: перебор активных записей + `overlaps()`. Между компаниями — допускается. |
|
||||
| Длина комментария | Не более 255 символов. | `src/validators.js` строка 183 (или в api/routes/entries.js): проверка `comment.length > 255` → 400. |
|
||||
|
||||
# Приложение А – Список запрещённых к созданию подсетей
|
||||
|
||||
> **Реализация:** `src/validators.js` строки 4–20 — массив `BLOCKED_RANGES`.
|
||||
|
||||
| Назначение | Префикс |
|
||||
| --- | --- |
|
||||
| Private (RFC1918) | 10.0.0.0/8 |
|
||||
| Private (RFC1918) | 172.16.0.0/12 |
|
||||
| Private (RFC1918) | 192.168.0.0/16 |
|
||||
| CGNAT (RFC6598) | 100.64.0.0/10 |
|
||||
| Loopback | 127.0.0.0/8 |
|
||||
| Link-local (APIPA) | 169.254.0.0/16 |
|
||||
| IANA special block | 192.0.0.0/24 |
|
||||
| TEST-NET-1 (docs) | 192.0.2.0/24 |
|
||||
| TEST-NET-2 (docs) | 198.51.100.0/24 |
|
||||
| TEST-NET-3 (docs) | 203.0.113.0/24 |
|
||||
| Benchmarking | 198.18.0.0/15 |
|
||||
| Multicast | 224.0.0.0/4 |
|
||||
| Reserved (Class E) | 240.0.0.0/4 |
|
||||
| Limited broadcast | 255.255.255.255/32 |
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Расхождения с ТЗ (что не реализовано)
|
||||
|
||||
| Пункт ТЗ | Статус |
|
||||
| --- | --- |
|
||||
| 3.1 Несколько компаний для одного пользователя | Не реализовано — ждём формат claim от devops |
|
||||
| 4.1 Фильтр soft-deleted записей для admin | Не реализовано — `includeDeleted` всегда false |
|
||||
| 4.7 /export без авторизации (по IP) | Не реализовано — требует Bearer-токен |
|
||||
@@ -0,0 +1,85 @@
|
||||
# План упрощения — 2026-06-11
|
||||
|
||||
## Проблема
|
||||
|
||||
AI усложнил код `entries.js` фоллбеками `getOrCreateCompany`, запутанной логикой `ownCo`,
|
||||
ненужными редиректами `?company=`. Пользователь указал что логика простая и не требует усложнений.
|
||||
|
||||
## Исходные данные (что юзер знает после входа по Keycloak)
|
||||
|
||||
1. **email** — из JWT (Keycloak claims)
|
||||
2. **session.token** — access_token от Keycloak
|
||||
3. **IAM API** (`fetchIamUser`) даёт:
|
||||
- `email`
|
||||
- `profiles[]` — список компаний юзера: `[{id, client_id, company_name, is_active_profile}, ...]`
|
||||
- `activeProfile` — текущая активная компания
|
||||
- `isAdmin` — флаг администратора
|
||||
|
||||
## Базовые правила
|
||||
|
||||
- **Компания** = общий список IP. Все юзеры компании видят одни и те же записи и могут CRUD.
|
||||
- **email** пишется в `audit_log.created_by` — кто сделал CRUD.
|
||||
- **client_id** (WZ01112, WZ03709…) — идентификатор компании в БД (`companies.client_id`).
|
||||
|
||||
## Переключение компаний (мульти-компания)
|
||||
|
||||
Если у юзера `profiles.length > 1` — он выбирает активную компанию через переключатель.
|
||||
CRUD идёт в активную компанию.
|
||||
|
||||
## Админ-режим (чекбокс → toggle)
|
||||
|
||||
### Правило (финальное):
|
||||
|
||||
> Toggle «Администратор» показывается **только** когда:
|
||||
> `isAdmin === true` **И** `activeClientId === 'WZ01112'`
|
||||
|
||||
### Временное исключение (тесты):
|
||||
|
||||
Пока я (WZ01112 + ntazetdinov@nubes.ru) — тоже вижу toggle, даже без `isAdmin`.
|
||||
Убрать когда перейдём на чистый IAM `isAdmin`.
|
||||
|
||||
### Поведение toggle:
|
||||
|
||||
- **ВЫКЛ** — обычный режим: юзер видит свою активную компанию, CRUD только в неё
|
||||
- **ВКЛ** — admin-режим: выпадающий список ВСЕХ компаний, можно выбрать любую, ссылки «Аудит» и «Лимиты»
|
||||
|
||||
### Реализация toggle:
|
||||
|
||||
- Не `<input type="checkbox">` с `onchange="this.form.submit()"` (старый вариант, не работал)
|
||||
- Замена на **toggle-переключатель** (как ползунок ВКЛ/ВЫКЛ)
|
||||
- `onchange` → `fetch('/toggle-admin', {method:'POST'})` → `location.reload()`
|
||||
- Никакой формы, никакого CSRF — просто JS fetch и перезагрузка страницы
|
||||
|
||||
## Что меняем (4 файла)
|
||||
|
||||
### 1. `ui/index.js` — resolveUser middleware
|
||||
|
||||
- Добавить `canAdminMode`:
|
||||
```js
|
||||
canAdminMode: (iamAdmin && activeClientId === 'WZ01112') || activeClientId === 'WZ01112' // временно
|
||||
```
|
||||
- Убрать `isAdmin: iamAdmin || activeClientId === ADMIN_CLIENT_ID` — заменить на чистое `isAdmin: iamAdmin`
|
||||
- `adminMode` остаётся из сессии
|
||||
|
||||
### 2. `views/index.ejs` — header
|
||||
|
||||
- Условие показа toggle: `<% if (canAdminMode) { %>` вместо `<% if (isAdmin) { %>`
|
||||
- Заменить checkbox на toggle
|
||||
- Ссылки «Аудит»/«Лимиты» — по `adminMode` (уже так)
|
||||
|
||||
### 3. `ui/routes/entries.js` — GET /
|
||||
|
||||
- **Убрать** `getOrCreateCompany`-фоллбек (строки 89-96)
|
||||
- **Упростить** admin-ветку: если нет `?company=` — показать свою компанию
|
||||
- Обычный пользователь: без изменений (уже работает)
|
||||
|
||||
### 4. `ui/routes/auth.js` — POST /toggle-admin
|
||||
|
||||
- Без изменений (уже работает — переключает `req.session.adminMode`)
|
||||
|
||||
## Почему именно так
|
||||
|
||||
1. **AND а не OR**: админ не должен видеть toggle в чужих компаниях — только в WZ01112
|
||||
2. **Toggle а не checkbox**: чекбокс = «прочитано/не прочитано», toggle = «режим ВКЛ/ВЫКЛ» — семантически верно
|
||||
3. **JS fetch а не form submit**: быстрее, без моргания страницы, без CSRF-проблем
|
||||
4. **Никаких фоллбеков**: компания либо есть в списке (из IAM), либо юзер к ней не относится — не надо «создавать если нет»
|
||||
@@ -0,0 +1,76 @@
|
||||
# Баг: переключение компании — таблица IP не обновляется
|
||||
|
||||
## Симптом
|
||||
GET /v2/app?switchTo=WZ30001 → таблица показывает записи старой компании.
|
||||
|
||||
## Найденная причина №1 (основная)
|
||||
Порядок кода в `v2/src/user/index.js`:
|
||||
|
||||
```js
|
||||
// 1. clId = req.clientId ← resolveContext выставил СТАРЫЙ activeClientId
|
||||
const clId = req.clientId;
|
||||
|
||||
// 2. crud.list() по СТАРОМУ clId — ещё до switchTo
|
||||
const { entries, used, limit } = await crud.list(clId, includeDeleted);
|
||||
|
||||
// 3. Потом switchTo обновляет сессию и делает redirect
|
||||
if (req.query.switchTo) {
|
||||
req.session.user.activeClientId = targetId;
|
||||
return req.session.save(() => res.redirect('/v2/app'));
|
||||
}
|
||||
```
|
||||
|
||||
Итог: первый запрос с ?switchTo загружает записи СТАРОЙ компании.
|
||||
Второй запрос (редирект /v2/app) должен уже использовать новый activeClientId — если ничто его не затирает.
|
||||
|
||||
## Найденная причина №2 (если ENV заданы)
|
||||
`v2/src/impersonation/index.js` — при тестовом режиме (IMPERSONATION_TARGET задан):
|
||||
|
||||
```js
|
||||
u.activeClientId = MAIN_COMPANY; // ← затирает то что записал switchTo
|
||||
```
|
||||
|
||||
Middleware запускается при КАЖДОМ запросе (включая редирект после switchTo)
|
||||
и сбрасывает activeClientId обратно в IMPERSONATION_COMPANY.
|
||||
|
||||
## Что проверить
|
||||
```bash
|
||||
echo $IMPERSONATION_COMPANY
|
||||
echo $IMPERSONATION_TARGET
|
||||
echo $IMPERSONATION_ORIGINAL
|
||||
```
|
||||
|
||||
Если заданы → причина №2 (middleware затирает сессию).
|
||||
Если не заданы → причина №1 (но тогда второй запрос после редиректа должен работать).
|
||||
|
||||
## Фикс причины №1
|
||||
В `v2/src/user/index.js` — переставить проверку switchTo ПЕРЕД crud.list():
|
||||
|
||||
```js
|
||||
// Сначала switchTo
|
||||
if (req.query.switchTo) {
|
||||
const targetId = req.query.switchTo;
|
||||
const allowed = isAdmin || (req.profiles || []).find(p => p.client_id === targetId);
|
||||
if (allowed) {
|
||||
req.session.user.activeClientId = targetId;
|
||||
return req.session.save(err => {
|
||||
if (err) console.error('[switchTo] save error:', err.message);
|
||||
res.redirect('/v2/app');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Потом загружать записи
|
||||
const { entries, used, limit } = await crud.list(clId, includeDeleted);
|
||||
```
|
||||
|
||||
## Фикс причины №2
|
||||
В `v2/src/impersonation/index.js` — не перезаписывать activeClientId если он уже есть:
|
||||
|
||||
```js
|
||||
// Вместо:
|
||||
u.activeClientId = MAIN_COMPANY;
|
||||
|
||||
// Сделать:
|
||||
u.activeClientId = u.activeClientId || MAIN_COMPANY;
|
||||
```
|
||||
@@ -0,0 +1,69 @@
|
||||
# WOWATER — развёртывание ipwhitelist-app (без воды)
|
||||
|
||||
> Для опытных DevOps. Минимум текста, только команды.
|
||||
|
||||
## 1. PostgreSQL
|
||||
|
||||
```bash
|
||||
sudo apt install -y postgresql
|
||||
DB_PASS=$(openssl rand -base64 24)
|
||||
sudo -u postgres psql -c "CREATE USER ipwhitelist WITH PASSWORD '${DB_PASS}';"
|
||||
sudo -u postgres psql -c "CREATE DATABASE ipwhitelist OWNER ipwhitelist;"
|
||||
```
|
||||
|
||||
## 2. Node.js + код
|
||||
|
||||
```bash
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo bash -
|
||||
sudo apt install -y nodejs
|
||||
git clone https://gitea.services.ngcloud.ru/Nail/ipwhitelist-app.git /opt/ipwhitelist
|
||||
cd /opt/ipwhitelist
|
||||
npm install --production
|
||||
sudo -u postgres psql -d ipwhitelist -f sql/schema.sql
|
||||
```
|
||||
|
||||
## 3. .env
|
||||
|
||||
```bash
|
||||
cp .env.example .env && chmod 600 .env
|
||||
```
|
||||
|
||||
Заполнить: `DB_PASS`, `SESSION_SECRET`, `KC_CLIENT_ID`, `KC_CLIENT_SECRET`, `APP_URL`, `IAM_API_URL`.
|
||||
|
||||
`DEV_MODE` → `false`.
|
||||
|
||||
## 4. nginx + Let's Encrypt
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name ваш-домен.ru;
|
||||
ssl_certificate /etc/letsencrypt/live/.../fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/.../privkey.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 5. PM2
|
||||
|
||||
```bash
|
||||
npm i -g pm2
|
||||
pm2 start server.js --name ipwhitelist
|
||||
pm2 save && pm2 startup
|
||||
```
|
||||
|
||||
## 6. Keycloak OIDC
|
||||
|
||||
Создать Confidential client в Keycloak (realm `cloud`):
|
||||
- `Valid Redirect URIs`: `https://ваш-домен.ru/callback`
|
||||
- `Client Authentication`: On
|
||||
- Записать `KC_CLIENT_ID` и `KC_CLIENT_SECRET` в `.env`
|
||||
|
||||
Готово. `pm2 restart ipwhitelist`.
|
||||
@@ -0,0 +1,209 @@
|
||||
# ТЗ-плюс — уточнения и дополнения от заказчика
|
||||
|
||||
> Основа: `docs/ТЗ.md`
|
||||
> Дата последнего обновления: 2026-06-04
|
||||
> Файл для фиксации уточнений, дополнений и решений.
|
||||
|
||||
---
|
||||
|
||||
## 1. Аутентификация — интеграция с IAM API
|
||||
|
||||
### 1.1. IAM API — единый сервис авторизации (УТОЧНЕНО 04.06.2026)
|
||||
|
||||
IAM (Identity & Access Management) — сервис авторизации экосистемы Nubes.
|
||||
Все смежные сервисы ходят в него для проверки компании пользователя.
|
||||
|
||||
**Три стенда:**
|
||||
|
||||
| Стенд | URL |
|
||||
|---|---|
|
||||
| Dev | `https://auth-api-dev.ngcloud.ru` |
|
||||
| Test | `https://auth-api-test.ngcloud.ru` |
|
||||
| Prod | `https://auth-api.ngcloud.ru` |
|
||||
|
||||
**Swagger:** `https://auth-api-dev.ngcloud.ru/api/v1/documentation/`
|
||||
|
||||
**Исходный источник:** CRM Elma (контакт, компания, отношение N:M).
|
||||
|
||||
**Статус:** 🟢 подтверждено
|
||||
|
||||
### 1.2. Поток аутентификации (УТОЧНЕНО 04.06.2026)
|
||||
|
||||
```
|
||||
Пользователь → Keycloak (SSO) → IAM (обмен code) → access_token + refresh_token
|
||||
│
|
||||
┌───────────────────────────────┘
|
||||
▼
|
||||
GET /api/v1/auth/user (Bearer access_token)
|
||||
│
|
||||
▼
|
||||
{ profiles[], userInfo }
|
||||
```
|
||||
|
||||
`access_token` от IAM (token_type: "access", TTL 12 часов) сразу годится для
|
||||
вызова `GET /api/v1/auth/user`. Никакого дополнительного обмена не требуется.
|
||||
|
||||
**Статус:** 🟢 подтверждено (Node.js HTTP 200, получен реальный ответ)
|
||||
|
||||
### 1.3. client_id приложения в Keycloak
|
||||
- **Источник:** HAR-файл
|
||||
- **Факт:** `client_id` в URL auth-запроса
|
||||
- **Статус:** 🔴 требует подтверждения
|
||||
|
||||
### 1.4. JWKS URL и Redirect URI
|
||||
- **Статус:** 🔴 требует подтверждения DevOps
|
||||
|
||||
---
|
||||
|
||||
## 2. Компании пользователя — IAM /auth/user
|
||||
|
||||
### 2.1. `GET /api/v1/auth/user` (УТОЧНЕНО 04.06.2026)
|
||||
|
||||
**Реальный ответ** (пользователь `tazetdinovn@gmail.com`, prod):
|
||||
|
||||
```json
|
||||
{
|
||||
"profiles": [
|
||||
{
|
||||
"id": 4357,
|
||||
"client_id": "WZ03709",
|
||||
"company_id": "019cc24a-727e-740f-b407-bec79dab4162",
|
||||
"company_name": "naeel_test",
|
||||
"company_numeric_id": 2645,
|
||||
"is_active_profile": true
|
||||
}
|
||||
],
|
||||
"userInfo": {
|
||||
"clientID": "WZ03709",
|
||||
"company": "naeel_test",
|
||||
"companyId": "019cc24a-727e-740f-b407-bec79dab4162",
|
||||
"companyNumericId": 2645,
|
||||
"email": "tazetdinovn@gmail.com",
|
||||
"isAdmin": false,
|
||||
"fio": {"fullName": "Тазетдинов Наиль Фаритович", "name": "Наиль", "secondName": "Фаритович", "surname": "Тазетдинов"},
|
||||
"contactId": "019cc268-6c6a-781e-8613-4bed4ec7cd20",
|
||||
"elmaUserId": 39715
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Маппинг полей для использования в коде:**
|
||||
|
||||
| Назначение | Откуда |
|
||||
|---|---|
|
||||
| Список WZ-кодов компаний | `profiles[].client_id` |
|
||||
| Активная компания | `profiles[].is_active_profile === true` |
|
||||
| ID для switch-profile | `profiles[].id` |
|
||||
| WZ активной компании | `userInfo.clientID` |
|
||||
| Название компании | `userInfo.company` |
|
||||
| UUID компании | `userInfo.companyId` |
|
||||
| Флаг админа | `userInfo.isAdmin` |
|
||||
| Email | `userInfo.email` |
|
||||
|
||||
**Статус:** 🟢 подтверждено
|
||||
|
||||
### 2.2. `POST /api/v1/user/switch-profile` (УТОЧНЕНО 04.06.2026)
|
||||
|
||||
Тело: `{"profile_id": 3120}` или `{"company_id": "uuid"}`.
|
||||
|
||||
**Статус:** 🟡 уточнено по Swagger, не тестировалось на живом multi-company юзере
|
||||
|
||||
### 2.3. ⚫ ClientID через запятую в JWT — УСТАРЕЛО
|
||||
|
||||
- **Было (01.06):** `ClientID: "WZ11125, WZ03816"` в claims JWT
|
||||
- **Стало (04.06):** JWT содержит только ОДНУ компанию (активную).
|
||||
Правильный источник списка — `profiles[]` из IAM API.
|
||||
- **Статус:** ⚫ отменено
|
||||
|
||||
### 2.4. ⚫ Admin = WZ01112 — УСТАРЕЛО
|
||||
|
||||
- **Было:** `clientId === 'WZ01112'` → админ
|
||||
- **Стало:** `userInfo.isAdmin` из IAM API
|
||||
- **Статус:** ⚫ отменено
|
||||
|
||||
---
|
||||
|
||||
## 3. Сетевая доступность
|
||||
|
||||
### 3.1. ddos-guard (УТОЧНЕНО 04.06.2026)
|
||||
|
||||
Все `*.ngcloud.ru` за ddos-guard. IP `81.200.23.210` не блокируется —
|
||||
проблема в HTTP-клиенте:
|
||||
|
||||
| Клиент | auth-api | deck-api-test |
|
||||
|---|---|---|
|
||||
| `curl` (без --http2) | ❌ 403 | ❌ 403 |
|
||||
| `curl --http2` | ✅ 200 | ❌ 403 |
|
||||
| `Node.js https` | ✅ 200 | ✅ 200 |
|
||||
| Браузер | ✅ 200 | ✅ 200 |
|
||||
|
||||
**Правило для curl:** `--http2` + `Accept: application/json`.
|
||||
|
||||
**Node.js (приложение):** работает без проблем.
|
||||
|
||||
**Статус:** 🟢 подтверждено
|
||||
|
||||
---
|
||||
|
||||
## 4. Инфраструктура и деплой
|
||||
|
||||
- **Платформа:** 🔴 уточнить
|
||||
- **CI/CD:** 🔴 уточнить
|
||||
- **Сетевое ограничение /export:** 🔴 уточнить
|
||||
|
||||
---
|
||||
|
||||
## 5. Функциональные уточнения
|
||||
|
||||
### 5.1. Мульти-компания в UI
|
||||
- Список компаний из `profiles[]`, активная по `is_active_profile`
|
||||
- Переключение: UI → `POST /switch-profile` на IAM → обновить сессию
|
||||
- **Статус:** 🟡 требует реализации
|
||||
|
||||
### 5.2. Soft-delete фильтр
|
||||
- `includeDeleted` всегда `false`
|
||||
- **Статус:** 🟡 не реализовано
|
||||
|
||||
### 5.3. Индивидуальные лимиты
|
||||
- **Статус:** 🔴 проверить
|
||||
|
||||
### 5.4. Уведомление о нормализации
|
||||
- **Статус:** 🔴 проверить
|
||||
|
||||
---
|
||||
|
||||
## 6. Тестовые данные
|
||||
|
||||
| Email | ClientID | Компания | Роль |
|
||||
|---|---|---|---|
|
||||
| `tazetdinovn@gmail.com` | WZ03709 | naeel_test | Пользователь |
|
||||
| `client@example.com` | WZ01325 | Тест | Клиент |
|
||||
| `admin@nubes.ru` | WZ01112 | Нубес | Админ |
|
||||
|
||||
### HAR-сессия
|
||||
- Файл: `Files/nubes_login.har`, пользователь `tazet@narod.ru`, deck-test
|
||||
|
||||
---
|
||||
|
||||
## 7. Принятые решения
|
||||
|
||||
| Дата | Решение |
|
||||
|---|---|
|
||||
| 01.06.2026 | Экспорт: txt, одна строка = один CIDR |
|
||||
| 01.06.2026 | Демо одобрено |
|
||||
| 02.06.2026 | Любой юзер компании редактирует whitelist всей компании |
|
||||
| 04.06.2026 | **IAM API — источник компаний.** `GET /auth/user` → `profiles[]`. `isAdmin` из `userInfo.isAdmin`. Переключение через `POST /switch-profile`. |
|
||||
| 04.06.2026 | ddos-guard: curl требует `--http2`, Node.js работает |
|
||||
|
||||
| 16.06.2026 | **UI: кнопка Показать/Скрыть удалённые.** Вместо чекбокса — кнопка-переключатель. |
|
||||
| 16.06.2026 | **UI: восстановление удалённых записей.** Кнопка ↺ у удалённых, `POST /restore/:id`. Лимит проверяется. Аудит: RESTORE. |
|
||||
| 16.06.2026 | **UI: время МСК.** `toLocaleString('ru', {timeZone:'Europe/Moscow'})` + метка «МСК». |
|
||||
|
||||
---
|
||||
|
||||
## Легенда статусов
|
||||
|
||||
| 🔴 | Требует уточнения |
|
||||
| 🟡 | Уточнено, не реализовано |
|
||||
| 🟢 | Реализовано / подтверждено |
|
||||
| ⚫ | Отменено / не актуально |
|
||||
@@ -0,0 +1,248 @@
|
||||
Техническое задание
|
||||
Микросервис управления доверенными адресами клиентов
|
||||
Self-service портал для указания клиентами доверенных IPv4-адресов и подсетей,
|
||||
исключаемых из блокировки на стороне облачного провайдера во время DDoS-атак
|
||||
|
||||
# 1. Назначение и цели
|
||||
|
||||
## 1.1. Назначение
|
||||
|
||||
Микросервис предоставляет клиентам облачного провайдера web-интерфейс для самостоятельного управления списком доверенных IPv4-адресов и подсетей. Записи из этого списка исключаются из автоматической блокировки сетевого взаимодействия системами фильтрации и митигации провайдера, что снижает количество ложноположительных срабатываний для легитимного трафика клиента.
|
||||
|
||||
> **Реализация:** `server.js` — точка входа, подключает все роуты. `ui/index.js` — UI-слой.
|
||||
|
||||
## 1.2. Цели
|
||||
|
||||
- Дать клиентам возможность самостоятельно поддерживать актуальный список доверенных IPv4-адресов, которые будут исключаться из фильтрации во время DDoS-атак.
|
||||
|
||||
> **Реализация:** `ui/routes/entries.js` — UI CRUD. `src/api/routes/entries.js` — REST API.
|
||||
|
||||
- Предоставить сетевым инженерам единую точку просмотра и управления списками доверенных клиентских белых IPv4-адресов.
|
||||
|
||||
> **Реализация:** `ui/routes/admin.js` — /admin и /audit UI. `src/api/routes/admin.js` — API /companies, /audit.
|
||||
|
||||
- Обеспечить машиночитаемую выдачу агрегированного (суммаризированного) списка для систем фильтрации трафика.
|
||||
|
||||
> **Реализация:** `src/routes/export.js` — GET /export. `src/validators.js` строки 125–181 — функция `aggregateCIDRs`.
|
||||
|
||||
# 2. Объем работ
|
||||
|
||||
- Web-страница / закладка в личном кабинете для управления whitelist-записями.
|
||||
|
||||
> **Реализация:** `views/index.ejs` — главная страница. `ui/routes/entries.js` — роуты GET /, POST /add, POST /edit/:id, POST /delete/:id.
|
||||
|
||||
- Авторизация через существующий экземпляр Keycloak (OIDC).
|
||||
|
||||
> **Реализация:** `src/auth.js` строки 220–260 — верификация JWT RS256 токена Keycloak. Строка 245: чтение claim `ClientID`. Строка 251: определение роли `isAdmin`.
|
||||
|
||||
- Валидация формы на стороне клиента и сервера.
|
||||
|
||||
> **Реализация:** `src/validators.js` строки 26–79 — серверная валидация `validate()`. `views/index.ejs` — атрибут `pattern` в поле ввода (клиентская).
|
||||
|
||||
- Внешний endpoint выдачи агрегированного списка. Выдача txt-файлом с переносом строки. Одна строка – один объект.
|
||||
|
||||
> **Реализация:** `src/routes/export.js` строки 27–64 — GET /export, Content-Type: text/plain, aggregateCIDRs → join('\n').
|
||||
|
||||
- Хранение записей, журнал аудита.
|
||||
|
||||
> **Реализация:** `sql/schema.sql` — таблицы `whitelist_entries`, `audit_log`. `src/queries.js` строки 211–230 — функция `logAudit`.
|
||||
|
||||
- Административное управление лимитами по компаниям.
|
||||
|
||||
> **Реализация:** `src/api/routes/admin.js` строки 27–35 — PATCH /companies/:id/limit. `src/queries.js` — функция `setLimit`. `views/admin.ejs` — UI формы лимитов.
|
||||
|
||||
# 3. Роли и права доступа
|
||||
|
||||
Роли определяются на основании claims в OIDC-токене Keycloak. Соответствие claim → роль настраивается на этапе развёртывания.
|
||||
|
||||
> **Реализация:** `src/auth.js` строка 31: `ADMIN_CLIENT_ID = process.env.ADMIN_CLIENT_ID || 'WZ01112'`. Строка 251: `isAdmin: clientId === ADMIN_CLIENT_ID`.
|
||||
|
||||
| Роль | Идентификация | Видимость записей | Права на изменение |
|
||||
| --- | --- | --- | --- |
|
||||
| Клиент (client) | clientId | Только записи компаний, к которым принадлежит пользователь. | Создание, редактирование и удаление записей своих компаний (в пределах лимита). |
|
||||
| Администратор (admin) | clientId = WZ01112 (Нубес) и отдельный чек-бокс | Записи всех компаний. | Создание, редактирование, удаление всех записей. Изменение лимита для отдельных компаний. |
|
||||
|
||||
> **Реализация ролей в API:** `src/api/routes/entries.js` — функция `resolveCompany()`: если `req.user.isAdmin && req.query.company` → берёт чужую компанию, иначе `getOrCreateCompany(clientId)`. `src/auth.js` строки 260–265 — middleware `requireAdmin`.
|
||||
|
||||
## 3.1. Принадлежность к компании
|
||||
|
||||
Принадлежность пользователя к компании определяется через IAM API (`GET /api/v1/auth/user`). Поддерживается сценарий, когда пользователь принадлежит нескольким компаниям: в этом случае в интерфейсе предусматривается переключатель активной компании, а все операции выполняются в контексте выбранной компании.
|
||||
|
||||
> **Реализация:** `src/auth.js` — функция `fetchIamUser(token)` вызывает `GET {IAM_API_URL}/api/v1/auth/user`, получает `profiles[]` со всеми компаниями и `userInfo` с данными пользователя. `userInfo.isAdmin` — флаг админа (вместо хардкода `WZ01112`).
|
||||
>
|
||||
> 🔧 **Требует доработки:** `fetchIamUser` ещё не реализована. Сейчас данные берутся из JWT (`payload.ClientID`). Правильный источник — IAM API.
|
||||
|
||||
Ожидаемые данные из IAM API:
|
||||
- `profiles[].client_id` — все компании пользователя
|
||||
- `profiles[].is_active_profile` — активная компания
|
||||
- `profiles[].id` — ID профиля для `POST /switch-profile`
|
||||
- `userInfo.clientID` — WZ-код активной компании
|
||||
- `userInfo.company` — название компании
|
||||
- `userInfo.companyId` — UUID компании
|
||||
- `userInfo.isAdmin` — флаг администратора
|
||||
- `userInfo.email` — идентификация пользователя для аудита
|
||||
|
||||
# 4. Функциональные требования
|
||||
|
||||
## 4.1. Просмотр списка записей
|
||||
|
||||
- Клиент видит таблицу записей активной компании; Администратор – записи всех компаний с фильтром по компании.
|
||||
|
||||
> **Реализация:** `ui/routes/entries.js` — GET /: для admin грузит `getAllCompanies()` + фильтр по `?company=<id>`. `views/index.ejs` — dropdown компаний для admin, таблица записей.
|
||||
|
||||
- Для каждой записи отображаются: значение (адрес/подсеть), комментарий (если есть), автор(email), дата создания, дата последнего изменения.
|
||||
|
||||
> **Реализация:** `views/index.ejs` строки 280–295 — колонки таблицы: `value_cidr`, `comment`, `created_by`, `created_at`, `updated_at`. Даты в timezone `Europe/Moscow`.
|
||||
|
||||
- Soft-deleted записи по умолчанию скрыты; для администратора предусмотрен фильтр для их отображения.
|
||||
|
||||
> **Реализация:** `src/queries.js` строки 25–31 — `listEntries(companyId, includeDeleted)`. SQL: `AND deleted_at IS NULL` когда `includeDeleted=false`.
|
||||
>
|
||||
> ⚠️ **Фильтр "показать удалённые" в UI не реализован** — `includeDeleted` всегда `false`.
|
||||
|
||||
- Отображается текущее использование лимита: «использовано X из N».
|
||||
|
||||
> **Реализация:** `src/api/routes/entries.js` строка 62: `res.json({ entries, limit, used: entries.length })`. `views/index.ejs` — блок статистики `<%= used %> / <%= limit %>`.
|
||||
|
||||
## 4.2. Создание записи
|
||||
|
||||
- Форма содержит поля: значение (IPv4-адрес или подсеть CIDR) и необязательный комментарий (до 255 символов).
|
||||
|
||||
> **Реализация:** `views/index.ejs` — форма POST /add с полями `value` и `comment`. Атрибут `maxlength="18"` на поле адреса.
|
||||
|
||||
- Значение проходит валидацию (см. раздел 5) на клиенте и обязательно повторно на сервере.
|
||||
|
||||
> **Реализация:** `src/validators.js` строки 26–79 — `validate(input)`. Вызывается в `src/queries.js` строка 33: `const { cidr, wasNormalized } = validate(rawValue)`.
|
||||
|
||||
- Перед сохранением проверяется: соблюдение лимита компании, отсутствие пересечений и дубликатов внутри компании, отсутствие принадлежности к запрещённым диапазонам.
|
||||
|
||||
> **Реализация:** `src/queries.js` строки 32–70 — `createEntry()`: блокировка строки компании (FOR UPDATE), проверка лимита (строки 43–46), проверка дубликатов и пересечений (строки 49–57), проверка запрещённых диапазонов в `validate()` (строки 73–75 validators.js).
|
||||
|
||||
- При успешном сохранении создаётся запись аудита.
|
||||
|
||||
> **Реализация:** `src/queries.js` строка 66: `logAudit(userEmail, companyId, 'CREATE', null, cidr, ...)`.
|
||||
|
||||
## 4.3. Редактирование записи
|
||||
|
||||
- Редактирование значения и комментария доступно компании в рамках своих прав.
|
||||
|
||||
> **Реализация:** `ui/routes/entries.js` — POST /edit/:id. `src/api/routes/entries.js` — PATCH /entries/:id. Modal в `views/index.ejs` — кнопка `.btn-edit`, event delegation в `<script>`.
|
||||
|
||||
- При изменении значения повторно выполняется полный набор проверок валидации и пересечений.
|
||||
|
||||
> **Реализация:** `src/queries.js` строки 77–115 — `updateEntry()`: валидация через `validate()`, проверка пересечений (исключая саму запись: `AND id <> $2`).
|
||||
|
||||
- Изменение фиксируется в журнале аудита с сохранением прежнего и нового значения.
|
||||
|
||||
> **Реализация:** `src/queries.js` строка 107: `logAudit(userEmail, companyId, 'UPDATE', old.value_cidr, cidr, entryId, ...)`.
|
||||
|
||||
## 4.4. Удаление записи (soft delete)
|
||||
|
||||
- Удаление выполняется как логическое (soft delete): запись помечается удалённой (deleted_at, deleted_by), но физически сохраняется.
|
||||
|
||||
> **Реализация:** `src/queries.js` строки 118–145 — `deleteEntry()`: UPDATE SET `deleted_at = NOW(), deleted_by = userEmail`. `sql/schema.sql` — колонки `deleted_at`, `deleted_by` в таблице `whitelist_entries`.
|
||||
|
||||
- Удалённая запись освобождает место в лимите компании и исключается из внешней агрегированной выдачи.
|
||||
|
||||
> **Реализация:** `src/queries.js` строка 50: COUNT считает только `WHERE deleted_at IS NULL`. `src/routes/export.js` — запрос только активных записей.
|
||||
|
||||
- Действие фиксируется в журнале аудита.
|
||||
|
||||
> **Реализация:** `src/queries.js` строка 134: `logAudit(userEmail, companyId, 'DELETE', old.value_cidr, null, ...)`.
|
||||
|
||||
## 4.5. Лимит записей на компанию
|
||||
|
||||
- Действует глобальный лимит по умолчанию: 15 активных записей на компанию.
|
||||
|
||||
> **Реализация:** `src/queries.js` строки 17–20 — `getLimit()`: `parseInt(process.env.DEFAULT_LIMIT) || 15`.
|
||||
|
||||
- Значение глобального лимита по умолчанию задаётся конфигурацией сервиса и может быть изменено без пересборки.
|
||||
|
||||
> **Реализация:** `src/queries.js` строка 18: `process.env.DEFAULT_LIMIT` — переменная окружения, не хардкод.
|
||||
|
||||
- Для отдельной компании администратор может задать индивидуальный лимит, переопределяющий глобальный (как в большую, так и в меньшую сторону).
|
||||
|
||||
> **Реализация:** `src/api/routes/admin.js` строки 27–35 — PATCH /companies/:id/limit. `src/queries.js` строка 20: `company.custom_limit != null ? company.custom_limit : defaultLimit`. `sql/schema.sql` — колонка `custom_limit` в таблице `companies`.
|
||||
|
||||
- При попытке превысить лимит создание блокируется с понятным сообщением; в подсчёт идут только активные записи.
|
||||
|
||||
> **Реализация:** `src/queries.js` строки 43–46: `if (cnt >= limit) throw new Error('Лимит исчерпан: N из N')`. API возвращает 409.
|
||||
|
||||
- Снижение лимита ниже текущего числа записей не удаляет существующие записи, но блокирует создание новых до приведения в соответствие.
|
||||
|
||||
> **Реализация:** `src/api/routes/admin.js` строки 28–35 — setLimit просто записывает значение без удаления записей. Блокировка создания — через проверку `cnt >= limit` в `createEntry`.
|
||||
|
||||
## 4.6. Журнал аудита
|
||||
|
||||
- Все изменяющие операции фиксируются неизменяемыми записями аудита.
|
||||
|
||||
> **Реализация:** `src/queries.js` строки 211–218 — `logAudit()`: INSERT в `audit_log` без UPDATE/DELETE операций над ней.
|
||||
|
||||
- Каждая запись аудита содержит: кто (пользователь), когда (timestamp), компания, тип действия, прежнее и новое состояние.
|
||||
|
||||
> **Реализация:** `sql/schema.sql` — таблица `audit_log`: колонки `user_email`, `created_at`, `company_id`, `action`, `old_value`, `new_value`. `views/audit.ejs` строки 131–160 — отображение.
|
||||
|
||||
- Журнал доступен для просмотра только администратору.
|
||||
|
||||
> **Реализация:** `src/api/routes/admin.js` — GET /audit защищён `apiRequireAdmin`. `ui/routes/admin.js` — GET /audit проверяет `req.session.user.isAdmin`.
|
||||
|
||||
## 4.7. Внешняя выдача агрегированного списка
|
||||
|
||||
- Подсети суммаризируются (агрегируются в минимальный набор CIDR) по всем компаниям совместно. Пересечения между разными компаниями допустимы.
|
||||
|
||||
> **Реализация:** `src/validators.js` строки 125–181 — `aggregateCIDRs()`: сортировка, слияние перекрывающихся диапазонов, преобразование обратно в CIDR.
|
||||
|
||||
- Предоставляется отдельный HTTP GET endpoint, отдающий полный суммаризированный список активных записей всех компаний файлом в формате txt.
|
||||
|
||||
> **Реализация:** `src/routes/export.js` строки 27–64 — GET /export. Content-Type: `text/plain`, ответ: `aggregated.join('\n')`.
|
||||
|
||||
- Авторизация: на старте endpoint может работать без авторизации (по сетевому ограничению / разрешенный список потребителей по ip).
|
||||
|
||||
> ⚠️ **Не реализовано** — /export требует Bearer-токен (авторизован). По ТЗ должен быть доступен без авторизации по IP-списку.
|
||||
|
||||
# 5. Требования к валидации
|
||||
|
||||
Валидация выполняется на клиенте и обязательно дублируется на сервере. Серверная валидация является авторитетной.
|
||||
|
||||
> **Реализация:** `src/validators.js` — вся серверная валидация. `views/index.ejs` — атрибут `pattern` (клиент).
|
||||
|
||||
| Правило | Описание | Реализация |
|
||||
| --- | --- | --- |
|
||||
| Формат IPv4 | Допускается одиночный адрес или подсеть CIDR /22–/32. | `src/validators.js` строки 41–53: добавление /32 если нет маски, проверка `mask < 22 \|\| mask > 32`. |
|
||||
| Только IPv4 | IPv6-значения или доменные имена отклоняются. | `src/validators.js` строки 31–36: `if (raw.includes(':'))` → отклонить, проверка букв. |
|
||||
| Корректность подсети | Host-биты обнуляются, пользователь уведомляется о нормализации. | `src/validators.js` строки 55–69: битовая арифметика, `wasNormalized = addr !== networkAddr`. Флаш-сообщение в `ui/routes/entries.js`. |
|
||||
| Запрет серых адресов | Диапазоны из Приложения А запрещены. | `src/validators.js` строки 4–20: `BLOCKED_RANGES[]`. Строки 73–75: проверка `overlaps()`. |
|
||||
| Отсутствие дубликатов | Совпадающие записи в компании запрещены. | `src/queries.js` строка 53: `if (row.value_cidr === cidr) throw`. Уникальный индекс в `sql/schema.sql`. |
|
||||
| Отсутствие пересечений | Пересечение с существующей записью в компании запрещено. | `src/queries.js` строки 49–57: перебор активных записей + `overlaps()`. Между компаниями — допускается. |
|
||||
| Длина комментария | Не более 255 символов. | `src/validators.js` строка 183 (или в api/routes/entries.js): проверка `comment.length > 255` → 400. |
|
||||
|
||||
# Приложение А – Список запрещённых к созданию подсетей
|
||||
|
||||
> **Реализация:** `src/validators.js` строки 4–20 — массив `BLOCKED_RANGES`.
|
||||
|
||||
| Назначение | Префикс |
|
||||
| --- | --- |
|
||||
| Private (RFC1918) | 10.0.0.0/8 |
|
||||
| Private (RFC1918) | 172.16.0.0/12 |
|
||||
| Private (RFC1918) | 192.168.0.0/16 |
|
||||
| CGNAT (RFC6598) | 100.64.0.0/10 |
|
||||
| Loopback | 127.0.0.0/8 |
|
||||
| Link-local (APIPA) | 169.254.0.0/16 |
|
||||
| IANA special block | 192.0.0.0/24 |
|
||||
| TEST-NET-1 (docs) | 192.0.2.0/24 |
|
||||
| TEST-NET-2 (docs) | 198.51.100.0/24 |
|
||||
| TEST-NET-3 (docs) | 203.0.113.0/24 |
|
||||
| Benchmarking | 198.18.0.0/15 |
|
||||
| Multicast | 224.0.0.0/4 |
|
||||
| Reserved (Class E) | 240.0.0.0/4 |
|
||||
| Limited broadcast | 255.255.255.255/32 |
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Расхождения с ТЗ (что не реализовано)
|
||||
|
||||
| Пункт ТЗ | Статус |
|
||||
| --- | --- |
|
||||
| 3.1 Несколько компаний для одного пользователя | Не реализовано — ждём формат claim от devops |
|
||||
| 4.1 Фильтр soft-deleted записей для admin | Не реализовано — `includeDeleted` всегда false |
|
||||
| 4.7 /export без авторизации (по IP) | Не реализовано — требует Bearer-токен |
|
||||
@@ -0,0 +1,97 @@
|
||||
Техническое задание
|
||||
Микросервис управления доверенными адресами клиентов
|
||||
Self-service портал для указания клиентами доверенных IPv4-адресов и подсетей,
|
||||
исключаемых из блокировки на стороне облачного провайдера во время DDoS-атак
|
||||
# 1. Назначение и цели
|
||||
## 1.1. Назначение
|
||||
Микросервис предоставляет клиентам облачного провайдера web-интерфейс для самостоятельного управления списком доверенных IPv4-адресов и подсетей. Записи из этого списка исключаются из автоматической блокировки сетевого взаимодействия системами фильтрации и митигации провайдера, что снижает количество ложноположительных срабатываний для легитимного трафика клиента.
|
||||
## 1.2. Цели
|
||||
- Дать клиентам возможность самостоятельно поддерживать актуальный список доверенных IPv4-адресов, которые будут исключаться из фильтрации во время DDoS-атак.
|
||||
- Предоставить сетевым инженерам единую точку просмотра и управления списками доверенных клиентских белых IPv4-адресов.
|
||||
- Обеспечить машиночитаемую выдачу агрегированного (суммаризированного) списка для систем фильтрации трафика.
|
||||
# 2. Объем работ
|
||||
- Web-страница / закладка в личном кабинете для управления whitelist-записями.
|
||||
- Авторизация через существующий экземпляр Keycloak (OIDC).
|
||||
- Валидация формы на стороне клиента и сервера.
|
||||
- Внешний endpoint выдачи агрегированного списка. Выдача txt-файлом с переносом строки. Одна строка – один объект.
|
||||
- Хранение записей, журнал аудита.
|
||||
- Административное управление лимитами по компаниям.
|
||||
# 3. Роли и права доступа
|
||||
Роли определяются на основании claims в OIDC-токене Keycloak. Соответствие claim → роль настраивается на этапе развёртывания.
|
||||
## 3.1. Принадлежность к компании
|
||||
Принадлежность пользователя к компании определяется из claim токена. Поддерживается сценарий, когда пользователь принадлежит нескольким компаниям: в этом случае в интерфейсе предусматривается переключатель активной компании, а все операции выполняются в контексте выбранной компании.
|
||||
Ожидаемые claims (имена согласуются с командой Keycloak):
|
||||
clientID — идентификатор компании
|
||||
email — идентификация пользователя для аудита
|
||||
# 4. Функциональные требования
|
||||
## 4.1. Просмотр списка записей
|
||||
- Клиент видит таблицу записей активной компании; Администратор – записи всех компаний с фильтром по компании.
|
||||
- Для каждой записи отображаются: значение (адрес/подсеть), комментарий (если есть), автор(email), дата создания, дата последнего изменения.
|
||||
- Soft-deleted записи по умолчанию скрыты; для администратора предусмотрен фильтр для их отображения.
|
||||
- Отображается текущее использование лимита: «использовано X из N».
|
||||
## 4.2. Создание записи
|
||||
- Форма содержит поля: значение (IPv4-адрес или подсеть CIDR) и необязательный комментарий (до 255 символов).
|
||||
- Значение проходит валидацию (см. раздел 5) на клиенте и обязательно повторно на сервере.
|
||||
- Перед сохранением проверяется: соблюдение лимита компании, отсутствие пересечений и дубликатов внутри компании, отсутствие принадлежности к запрещённым диапазонам.
|
||||
- При успешном сохранении создаётся запись аудита.
|
||||
## 4.3. Редактирование записи
|
||||
- Редактирование значения и комментария доступно компании в рамках своих прав.
|
||||
- При изменении значения повторно выполняется полный набор проверок валидации и пересечений.
|
||||
- Изменение фиксируется в журнале аудита с сохранением прежнего и нового значения.
|
||||
## 4.4. Удаление записи (soft delete)
|
||||
- Удаление выполняется как логическое (soft delete): запись помечается удалённой (deleted_at, deleted_by), но физически сохраняется.
|
||||
- Удалённая запись освобождает место в лимите компании и исключается из внешней агрегированной выдачи.
|
||||
- Действие фиксируется в журнале аудита.
|
||||
## 4.5. Лимит записей на компанию
|
||||
- Действует глобальный лимит по умолчанию: 15 активных записей на компанию.
|
||||
- Значение глобального лимита по умолчанию задаётся конфигурацией сервиса и может быть изменено без пересборки.
|
||||
- Для отдельной компании администратор может задать индивидуальный лимит, переопределяющий глобальный (как в большую, так и в меньшую сторону).
|
||||
- При попытке превысить лимит создание блокируется с понятным сообщением; в подсчёт идут только активные записи.
|
||||
- Снижение лимита ниже текущего числа записей не удаляет существующие записи, но блокирует создание новых до приведения в соответствие.
|
||||
## 4.6. Журнал аудита
|
||||
- Все изменяющие операции фиксируются неизменяемыми записями аудита.
|
||||
- Каждая запись аудита содержит: кто (пользователь), когда (timestamp), компания, тип действия, прежнее и новое состояние.
|
||||
- Журнал доступен для просмотра только администратору.
|
||||
## 4.7. Внешняя выдача агрегированного списка
|
||||
- Подсети суммаризируются (агрегируются в минимальный набор CIDR) по всем компаниям совместно. Пересечения между разными компаниями допустимы.
|
||||
- Предоставляется отдельный HTTP GET endpoint, отдающий полный суммаризированный список активных записей всех компаний файлом в формате txt.
|
||||
- Авторизация: на старте endpoint может работать без авторизации (по сетевому ограничению / разрешенный список потребителей по ip).
|
||||
# 5. Требования к валидации
|
||||
Валидация выполняется на клиенте и обязательно дублируется на сервере. Серверная валидация является авторитетной.
|
||||
|
||||
# Приложение А – Список запрещенных к созданию подсетей.
|
||||
|
||||
|
||||
| Роль | Идентификация | Видимость записей | Права на изменение |
|
||||
| --- | --- | --- | --- |
|
||||
| Клиент (client) | clientId | Только записи компаний, к которым принадлежит пользователь. | Создание, редактирование и удаление записей своих компаний (в пределах лимита). |
|
||||
| Администратор (admin) | clientId = WZ01112 (Нубес) и отдельный чек-бокс | Записи всех компаний. | Создание, редактирование, удаление всех записей. Изменение лимита для отдельных компаний. |
|
||||
|
||||
|
||||
| Правило | Описание |
|
||||
| --- | --- |
|
||||
| Формат IPv4 | Допускается одиночный адрес (например 203.0.113.10) или подсеть в нотации CIDR (например 203.0.113.0/24). Допускается использование масок /32 - /22. Маска /21 и больше не допускается. |
|
||||
| Только IPv4 | IPv6-значения или доменные имена отклоняются. |
|
||||
| Корректность подсети | Введенный адрес с маской подсети должен нормализоваться к адресу подсети, все host-биты должны быть обнулены. Пользователь должен быть уведомлен, что ввел адрес из хостовой части, а не адрес подсети и произошла нормализация. |
|
||||
| Запрет серых адресов | Адреса и подсети из частных диапазонов (Приложение А) запрещены к добавлению. |
|
||||
| Отсутствие дубликатов | В пределах одной компании запрещены полностью совпадающие записи. |
|
||||
| Отсутствие пересечений | В пределах одной компании запрещено добавление записи, пересекающейся с уже существующей (включая вложенность подсетей). Между разными компаниями пересечения допускаются. |
|
||||
| Длина комментария | Не более 255 символов; поле необязательное. |
|
||||
|
||||
|
||||
| Назначение | Префикс |
|
||||
| --- | --- |
|
||||
| Private (RFC1918) | 10.0.0.0/8 |
|
||||
| Private (RFC1918) | 172.16.0.0/12 |
|
||||
| Private (RFC1918) | 192.168.0.0/16 |
|
||||
| CGNAT (RFC6598) | 100.64.0.0/10 |
|
||||
| Loopback | 127.0.0.0/8 |
|
||||
| Link-local (APIPA) | 169.254.0.0/16 |
|
||||
| IANA special block | 192.0.0.0/24 |
|
||||
| TEST-NET-1 (docs) | 192.0.2.0/24 |
|
||||
| TEST-NET-2 (docs) | 198.51.100.0/24 |
|
||||
| TEST-NET-3 (docs) | 203.0.113.0/24 |
|
||||
| Benchmarking | 198.18.0.0/15 |
|
||||
| Multicast | 224.0.0.0/4 |
|
||||
| Reserved (Class E) | 240.0.0.0/4 |
|
||||
| Limited broadcast | 255.255.255.255/32 |
|
||||
@@ -0,0 +1,33 @@
|
||||
# Локальный Keycloak для тестирования OIDC
|
||||
|
||||
Поднимается на ВМ `italo.kube5s.ru` (5.172.178.213), пока нет кредов от продакшен-KC.
|
||||
|
||||
## Установка
|
||||
|
||||
```bash
|
||||
# Всё делается на ВМ:
|
||||
cd ~/keycloak
|
||||
./setup.sh
|
||||
```
|
||||
|
||||
## Доступ после запуска
|
||||
|
||||
| Что | URL |
|
||||
|---|---|
|
||||
| Keycloak admin | `https://italo.kube5s.ru:8443/admin` |
|
||||
| Realm | `ipwhitelist` |
|
||||
| OIDC endpoints | `https://italo.kube5s.ru:8443/realms/ipwhitelist/.well-known/openid-configuration` |
|
||||
|
||||
## Переменные для .env (ipwhitelist)
|
||||
|
||||
```
|
||||
KC_BASE_URL=https://italo.kube5s.ru:8443/realms/ipwhitelist
|
||||
KC_CLIENT_ID=ipwhitelist
|
||||
KC_CLIENT_SECRET=<из Keycloak admin>
|
||||
APP_URL=https://italo.kube5s.ru
|
||||
DEV_MODE=false
|
||||
```
|
||||
|
||||
## Пользователи
|
||||
|
||||
Создаются через Keycloak admin UI после запуска.
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ipwhitelist",
|
||||
"version": "0.1.0",
|
||||
"version": "0.4.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ipwhitelist",
|
||||
"version": "0.1.0",
|
||||
"version": "0.4.0",
|
||||
"dependencies": {
|
||||
"connect-pg-simple": "^10.0.0",
|
||||
"cookie-parser": "^1.4.7",
|
||||
@@ -19,6 +19,30 @@
|
||||
"helmet": "^8.2.0",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"pg": "^8.13.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"supertest": "^7.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
||||
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@paralleldrive/cuid2": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz",
|
||||
"integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@noble/hashes": "^1.1.5"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
@@ -38,11 +62,23 @@
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
|
||||
},
|
||||
"node_modules/asap": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
|
||||
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
@@ -119,6 +155,27 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/component-emitter": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
|
||||
"integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/connect-pg-simple": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/connect-pg-simple/-/connect-pg-simple-10.0.0.tgz",
|
||||
@@ -179,6 +236,12 @@
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
|
||||
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="
|
||||
},
|
||||
"node_modules/cookiejar": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz",
|
||||
"integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/csrf-csrf": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/csrf-csrf/-/csrf-csrf-4.0.3.tgz",
|
||||
@@ -195,6 +258,15 @@
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
@@ -212,6 +284,16 @@
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/dezalgo": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
|
||||
"integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"asap": "^2.0.0",
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.6.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||
@@ -298,6 +380,21 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
@@ -395,6 +492,12 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-safe-stringify": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
|
||||
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/filelist": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz",
|
||||
@@ -420,6 +523,39 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/formidable": {
|
||||
"version": "3.5.4",
|
||||
"resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz",
|
||||
"integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"dezalgo": "^1.0.4",
|
||||
"once": "^1.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://ko-fi.com/tunnckoCore/commissions"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
@@ -501,6 +637,21 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||
@@ -786,6 +937,15 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
@@ -1142,6 +1302,84 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/superagent": {
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz",
|
||||
"integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"component-emitter": "^1.3.1",
|
||||
"cookiejar": "^2.1.4",
|
||||
"debug": "^4.3.7",
|
||||
"fast-safe-stringify": "^2.1.1",
|
||||
"form-data": "^4.0.5",
|
||||
"formidable": "^3.5.4",
|
||||
"methods": "^1.1.2",
|
||||
"mime": "2.6.0",
|
||||
"qs": "^6.14.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/superagent/node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/superagent/node_modules/mime": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
|
||||
"integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"mime": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/superagent/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/supertest": {
|
||||
"version": "7.2.2",
|
||||
"resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz",
|
||||
"integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cookie-signature": "^1.2.2",
|
||||
"methods": "^1.1.2",
|
||||
"superagent": "^10.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/supertest/node_modules/cookie-signature": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
|
||||
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
@@ -1197,6 +1435,12 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
{
|
||||
"name": "ipwhitelist",
|
||||
"version": "0.2.0",
|
||||
"version": "0.5.163",
|
||||
"description": "IP WhiteList microservice for cloud provider",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "node --watch server.js"
|
||||
"dev": "node --watch server.js",
|
||||
"test": "node tests/api.js",
|
||||
"test:stress": "node tests/stress.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"connect-pg-simple": "^10.0.0",
|
||||
@@ -19,5 +21,8 @@
|
||||
"helmet": "^8.2.0",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"pg": "^8.13.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"supertest": "^7.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
Forbidden
|
||||
Transaction ID: bdd7fbca-219a-4dbf-95ca-a27cc8bdecd9
|
||||
@@ -1,209 +0,0 @@
|
||||
# SSO / Auth — исследование и контекст
|
||||
|
||||
> Дата: 2026-05-30
|
||||
> Актуально для ветки `sonnet`
|
||||
|
||||
---
|
||||
|
||||
## Исходные данные от команды
|
||||
|
||||
### Ссылки (получены 2026-05-30)
|
||||
|
||||
| Что | URL | Примечание |
|
||||
|---|---|---|
|
||||
| Пример SSO на ColdFusion | https://gitea.services.ngcloud.ru/smishchuk/payg-report.git | Написан «на коленке», но рабочий |
|
||||
| Конфигурация сервиса в Deck | https://deck.ngcloud.ru/services/instance/detail/523413d2-1ffd-457b-b0e3-0a8185fe9358 | Там видно как кладутся env-переменные |
|
||||
|
||||
### Файлы из payg-report сохранены локально
|
||||
|
||||
```
|
||||
research/payg-sso/
|
||||
Application.cfc — основной SSO flow (редирект, обмен code→token, refresh, logout)
|
||||
lib_oauth2.cfc — Authorization Code Flow клиент
|
||||
lib_jwt.cfc — валидация JWT RS256
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Keycloak — эндпоинты
|
||||
|
||||
Realm: `cloud`
|
||||
Base: `https://keycloak.nubes.ru/realms/cloud`
|
||||
|
||||
| Назначение | URL |
|
||||
|---|---|
|
||||
| Авторизация (браузер → KK) | `https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/auth` |
|
||||
| Обмен code → token | `https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/token` |
|
||||
| Logout | `https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/logout` |
|
||||
| JWKS (публичные ключи) | `https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/certs` |
|
||||
| OpenID конфигурация | `https://keycloak.nubes.ru/realms/cloud/.well-known/openid-configuration` |
|
||||
|
||||
---
|
||||
|
||||
## Реальная схема аутентификации
|
||||
|
||||
```
|
||||
Браузер
|
||||
│
|
||||
│ 1. GET / (без токена)
|
||||
▼
|
||||
наше приложение
|
||||
│
|
||||
│ 2. redirect → KK /auth?client_id=...&redirect_uri=...&scope=openid
|
||||
▼
|
||||
Keycloak (keycloak.nubes.ru, realm=cloud)
|
||||
│
|
||||
│ 3. Пользователь логинится
|
||||
│ 4. KK redirect → наш /callback?code=...
|
||||
▼
|
||||
наше приложение
|
||||
│
|
||||
│ 5. POST /token (code + client_secret) → access_token + refresh_token
|
||||
│ 6. Валидация JWT через JWKS (certs endpoint)
|
||||
│ 7. Извлечение claims из JWT → req.user
|
||||
▼
|
||||
Работаем: req.user.ClientID = "WZ01325" и тд
|
||||
```
|
||||
|
||||
**Важно из примера payg-report:**
|
||||
- Тип flow: Authorization Code Flow (не Implicit, не PKCE — обычный)
|
||||
- `client_secret` передаётся при обмене code→token (confidential client)
|
||||
- Сертификат берётся с JWKS endpoint при старте, кэшируется в application scope
|
||||
- При истечении части времени жизни — автоматический refresh через refresh_token
|
||||
|
||||
---
|
||||
|
||||
## JWT — структура токена
|
||||
|
||||
JWT выпускает **auth-api** (не Keycloak напрямую).
|
||||
Алгоритм: **RS256**.
|
||||
JWKS для валидации: из auth-api (уточнить URL у DevOps).
|
||||
|
||||
> ⚠️ В payg-report используют `keycloak.nubes.ru/certs`, но наш портал deck.ngcloud.ru
|
||||
> идёт через auth-api.ngcloud.ru — нужно уточнить чей именно JWT будет у нас.
|
||||
|
||||
### Claims в JWT (из реального токена, secrets.txt)
|
||||
|
||||
| Claim | Тип | Пример | Назначение |
|
||||
|---|---|---|---|
|
||||
| `iss` | string | `"auth-api"` | Издатель |
|
||||
| `sub` | string | `"0199e325-1cdf-..."` | UUID пользователя |
|
||||
| `exp` | number | `1795627443` | Срок действия |
|
||||
| `iat` | number | `1780075443` | Выпущен |
|
||||
| `ClientID` | string | `"WZ01325"` | **WZ-номер компании** ← ключевой claim |
|
||||
| `company_id` | string (UUID) | `"3e64aac6-dcfc-..."` | UUID компании |
|
||||
| `company_name` | string | `"Тест"` | Название компании |
|
||||
| `email` | string | `"tazet@narod.ru"` | Email (для аудита) |
|
||||
| `login` | string | `"tazet@narod.ru"` | Логин |
|
||||
| `firstname` | string | `"Наиль"` | Имя |
|
||||
| `lastname` | string | `"Тазетдинов"` | Фамилия |
|
||||
| `token_type` | string | `"tech"` | Тип токена |
|
||||
|
||||
**Пример декодированного payload** (из secrets.txt — не секрет, payload JWT не зашифрован):
|
||||
```json
|
||||
{
|
||||
"iss": "auth-api",
|
||||
"sub": "0199e325-1cdf-7cda-9319-e5302a85e291",
|
||||
"ClientID": "WZ01325",
|
||||
"company_id": "3e64aac6-dcfc-4082-88dc-da19c86555a5",
|
||||
"company_name": "Тест",
|
||||
"email": "tazet@narod.ru",
|
||||
"login": "tazet@narod.ru",
|
||||
"token_type": "tech"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Конфигурация сервиса (env-переменные)
|
||||
|
||||
Из примера payg-report и Deck — конфиги кладутся в переменные окружения:
|
||||
|
||||
```bash
|
||||
# Нужно получить от DevOps:
|
||||
KC_CLIENT_ID=white.nodejsk8s.dev.nubes.ru # имя нашего клиента в KK (уточнить)
|
||||
KC_CLIENT_SECRET=<секрет> # из Deck / у DevOps
|
||||
JWKS_URL=https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/certs
|
||||
# ИЛИ если через auth-api:
|
||||
# JWKS_URL=https://auth-api.ngcloud.ru/.well-known/jwks.json
|
||||
|
||||
# В payg-report переменная называется:
|
||||
IDP_CLIENT_SECRET=<секрет>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Что сейчас в нашем коде
|
||||
|
||||
Файл: `src/auth.js`
|
||||
|
||||
| Режим | Условие | Поведение |
|
||||
|---|---|---|
|
||||
| **DEV_MODE** | `DEV_MODE=true` и не production | Авторизация пропущена, юзер захардкожен из `MOCK_USERS` |
|
||||
| **JWKS_URL задан** | `JWKS_URL != ''` | Валидирует Bearer-токен из заголовка через JWKS |
|
||||
| **Мок RS256** | Ничего из выше | Генерирует собственную RSA-пару, выдаёт и валидирует JWT сам |
|
||||
|
||||
**Чего не хватает для продакшена:**
|
||||
- Authorization Code Flow (редирект на KK и обратно)
|
||||
- Обмен `code` → `access_token` через `/token`
|
||||
- Session/cookie для хранения токена между запросами
|
||||
- Refresh токена
|
||||
|
||||
---
|
||||
|
||||
## Пример SSO flow из payg-report (ключевые строки)
|
||||
|
||||
```javascript
|
||||
// Настройка (из окружения):
|
||||
const client_id = process.env.IDP_CLIENT_SECRET; // их название
|
||||
const auth_endpoint = 'https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/auth';
|
||||
const token_endpoint = 'https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/token';
|
||||
const certs_url = 'https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/certs';
|
||||
|
||||
// 1. Нет сессии → redirect на KK:
|
||||
location(idp.buildRedirectToAuthURL({scope: 'openid profile email', state: guid}));
|
||||
|
||||
// 2. KK вернул ?code=... → меняем на токен:
|
||||
const resp = idp.makeAccessTokenRequest(url.code);
|
||||
|
||||
// 3. Парсим JWT из ответа:
|
||||
const token_data = jwt.decode(resp.access_token, idpCertificate, 'RS256');
|
||||
session.auth.wz = token_data.ClientID; // WZ-номер компании
|
||||
session.auth.login = token_data.preferred_username;
|
||||
|
||||
// 4. Refresh когда прошло > 2/3 времени жизни (только на GET):
|
||||
if (timeSinceIat > refresh_expires_in / 1.5 && method === 'GET') {
|
||||
const resp = idp.refreshAccessTokenRequest(session.auth.refresh_token);
|
||||
// ... обновляем сессию
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Открытые вопросы (нужен ответ от DevOps/KK-команды)
|
||||
|
||||
| # | Вопрос | Влияет на |
|
||||
|---|---|---|
|
||||
| 1 | Какой `client_id` выдан нашему сервису в KK realm `cloud`? | Конфиг |
|
||||
| 2 | Где взять `client_secret`? | Конфиг |
|
||||
| 3 | Токен от KK или от auth-api? Соответственно — какой JWKS_URL? | `src/auth.js` |
|
||||
| 4 | Claim для admin-роли: чек-бокс — это `realm_roles`, `resource_access`, attribute? | `src/auth.js` requireAdmin |
|
||||
| 5 | `ClientID` в токене — строка или массив (multi-company)? | `src/queries.js`, UI |
|
||||
| 6 | `/export` — нужна ли авторизация или только сетевое ограничение по IP? | `src/routes/export.js` |
|
||||
|
||||
---
|
||||
|
||||
## API Gateway (для справки)
|
||||
|
||||
Из `docs/auth-architecture.md`:
|
||||
- Gateway: `lk-api-gateway.ngcloud.ru`
|
||||
- JWT из localStorage портала: `authApiTokens.access_token`
|
||||
- **DDOS-Guard cookies обязательны** при curl-тестировании: `__ddg1_`, `__ddg8_`, `__ddg9_`, `__ddg10_`
|
||||
|
||||
```bash
|
||||
# Пример рабочего curl (2026-05-29):
|
||||
curl -H "Authorization: Bearer <JWT>" \
|
||||
-H "Origin: https://deck.ngcloud.ru" \
|
||||
-H "Cookie: __ddg1_=...; __ddg8_=...; __ddg9_=...; __ddg10_=..." \
|
||||
"https://lk-api-gateway.ngcloud.ru/api/v1/..."
|
||||
```
|
||||
@@ -1,357 +0,0 @@
|
||||
# Full Code Review — IP WhiteList (Opus, 2026-05-30)
|
||||
|
||||
> Ревью по коду из `prompt-opus-full-review-2026-05-30.md` (ветка `sonnet`).
|
||||
> Легенда: ✅ хорошо · ⚠️ замечание · ❌ проблема (блокер/риск).
|
||||
|
||||
---
|
||||
|
||||
## 0. Краткое резюме (TL;DR)
|
||||
|
||||
Проект аккуратно структурирован: фабрики роутеров с DI, транзакции с `FOR UPDATE`,
|
||||
параметризованные запросы, частичные уникальные индексы для защиты от гонок.
|
||||
Базовая гигиена SQL/изоляции компаний — на хорошем уровне.
|
||||
|
||||
Однако к продакшену проект **не готов**. Найдено несколько серьёзных проблем:
|
||||
|
||||
| # | Проблема | Severity |
|
||||
|---|----------|----------|
|
||||
| 1 | `/export` смонтирован **до** auth-middleware → публичная выгрузка CIDR **всех** компаний | ❌ Критично |
|
||||
| 2 | CSRF `getSessionIdentifier` читает несуществующую cookie `jwt` → токен не привязан к сессии | ❌ Критично |
|
||||
| 3 | Нет `session.regenerate()` при логине → session fixation | ❌ Высокий |
|
||||
| 4 | Open redirect через `returnTo` | ❌ Высокий |
|
||||
| 5 | OIDC: нет проверки `issuer`/`audience`, нет matching по `kid`, JWKS не рефрешится и null в bearer-режиме | ❌ Высокий |
|
||||
| 6 | `ssl: { rejectUnauthorized: false }` к БД | ⚠️/❌ |
|
||||
| 7 | Дефолтные секреты (`SESSION_SECRET`, `CSRF_SECRET`) не fail-fast в проде | ⚠️ |
|
||||
| 8 | CSP отключён (`contentSecurityPolicy: false`) | ⚠️ |
|
||||
| 9 | Нет интеграционных тестов (auth, CSRF, IDOR, export) | ⚠️ |
|
||||
|
||||
---
|
||||
|
||||
## 1. Безопасность (OWASP Top 10)
|
||||
|
||||
### 1.1 ❌ Публичная выгрузка всех компаний через `/export`
|
||||
|
||||
В `server.js` порядок монтирования:
|
||||
|
||||
```js
|
||||
app.use(require('./src/routes/export').createRouter({ q, exportLimiter, aggregateCIDRs }));
|
||||
// ...
|
||||
app.use(auth.middleware); // ← аутентификация ПОСЛЕ export
|
||||
```
|
||||
|
||||
`/export` доступен **без аутентификации**, а внутри:
|
||||
|
||||
```js
|
||||
const cidrs = await q.getExportCIDRs(); // companyId = null → ВСЕ компании
|
||||
const aggregated = aggregateCIDRs(cidrs);
|
||||
```
|
||||
|
||||
`getExportCIDRs(null)` возвращает CIDR **всех** компаний, агрегированные вместе.
|
||||
Любой анонимный пользователь получает полный список whitelisted-IP всех арендаторов.
|
||||
Это нарушение изоляции данных (A01 Broken Access Control) и утечка информации (A01/A04).
|
||||
|
||||
**Рекомендация:** одно из:
|
||||
- перенести `/export` **после** `auth.middleware` и фильтровать по `req.user` (для админа — все/выбранная компания, для пользователя — только своя);
|
||||
- либо, если выгрузка для оборудования должна быть машинной, защитить статическим bearer-токеном/mTLS и **никогда** не отдавать срез всех компаний без явной авторизации.
|
||||
|
||||
### 1.2 ❌ CSRF: `getSessionIdentifier` привязан к мёртвой cookie
|
||||
|
||||
```js
|
||||
// src/middleware/csrf.js
|
||||
getSessionIdentifier: (req) => req.cookies.jwt || '',
|
||||
```
|
||||
|
||||
В коде есть честный комментарий, что после перехода на `express-session` cookie `jwt`
|
||||
больше не выдаётся. Значит идентификатор сессии для **всех** пользователей = `''`.
|
||||
Double-submit перестаёт быть привязан к конкретной сессии — токен валиден «глобально»,
|
||||
что ослабляет защиту (особенно с учётом session fixation ниже).
|
||||
|
||||
**Рекомендация:**
|
||||
```js
|
||||
getSessionIdentifier: (req) => req.session?.id || req.sessionID || '',
|
||||
```
|
||||
и убедиться, что `initCsrf()` вызывается после подключения `session` middleware (сейчас так и есть).
|
||||
|
||||
### 1.3 ❌ Session fixation — нет регенерации сессии при логине
|
||||
|
||||
В `routes/auth.js` (POST `/login`, `/callback`, `/dev-login`) сразу пишется
|
||||
`req.session.user = ...` без `req.session.regenerate()`. Идентификатор сессии,
|
||||
выданный до аутентификации, сохраняется — классический session fixation (A07).
|
||||
|
||||
**Рекомендация:** перед установкой `user` вызывать:
|
||||
```js
|
||||
req.session.regenerate(err => { if (err) ...; req.session.user = user; req.session.save(() => res.redirect(...)); });
|
||||
```
|
||||
|
||||
### 1.4 ❌ Open redirect через `returnTo`
|
||||
|
||||
```js
|
||||
// POST /login
|
||||
res.redirect(req.query.returnTo || '/');
|
||||
// /callback
|
||||
const returnTo = req.session.returnTo || '/';
|
||||
res.redirect(returnTo);
|
||||
```
|
||||
|
||||
`returnTo` приходит из запроса и не валидируется. Значение вида `//evil.com`
|
||||
или `https://evil.com` приведёт к открытому редиректу (A01, фишинг).
|
||||
|
||||
**Рекомендация:** разрешать только локальные пути:
|
||||
```js
|
||||
function safeReturn(t) {
|
||||
return (typeof t === 'string' && t.startsWith('/') && !t.startsWith('//')) ? t : '/';
|
||||
}
|
||||
```
|
||||
|
||||
### 1.5 ❌ OIDC verification — недостаточная проверка токена
|
||||
|
||||
```js
|
||||
function verifyOidcToken(token) {
|
||||
const key = cachedJwks.keys.find(k => k.kty === 'RSA' && k.use === 'sig'); // ← не по kid
|
||||
...
|
||||
return jwt.verify(token, pem, { algorithms: ['RS256'] }); // ← нет issuer/audience
|
||||
}
|
||||
```
|
||||
|
||||
Проблемы:
|
||||
- **Нет проверки `issuer` и `audience`.** Любой RS256-токен, подписанный ключом из этого JWKS
|
||||
(например, токен, выданный другому клиенту того же realm), пройдёт проверку → privilege/tenant confusion.
|
||||
- **Выбор ключа не по `kid`** из заголовка токена, а «первый RSA sig». При ротации/нескольких
|
||||
ключах возможны как ложные отказы, так и приём не того ключа.
|
||||
- **`cachedJwks` загружается только в `exchangeCode`** и больше не рефрешится. В bearer-режиме
|
||||
(запрос с `Authorization: Bearer` без предварительного `/callback`) `cachedJwks === null` →
|
||||
`verifyOidcToken` бросит `JWKS not loaded yet`. При ротации ключей в KC — отказы до рестарта.
|
||||
|
||||
**Рекомендация:** грузить JWKS при старте и кэшировать с TTL/refresh по `kid`; в `jwt.verify`
|
||||
передавать `{ issuer: KC_ISSUER, audience: KC_CLIENT_ID, algorithms: ['RS256'] }`; выбирать ключ
|
||||
по `kid` из декодированного заголовка.
|
||||
|
||||
### 1.6 ⚠️ TLS к БД отключает проверку сертификата
|
||||
|
||||
```js
|
||||
ssl: process.env.DB_SSLMODE === 'require' ? { rejectUnauthorized: false } : false,
|
||||
```
|
||||
|
||||
`rejectUnauthorized: false` = шифрование без аутентификации сервера → MITM возможен (A02/A05).
|
||||
Имя `require` обманчиво: это поведение `sslmode=require` в libpq, но для прод-окружения
|
||||
нужен `verify-full` с CA.
|
||||
|
||||
**Рекомендация:** добавить режим с CA: `{ ca: fs.readFileSync(DB_CA), rejectUnauthorized: true }`.
|
||||
|
||||
### 1.7 ⚠️ Дефолтные секреты не приводят к отказу в проде
|
||||
|
||||
```js
|
||||
secret: process.env.SESSION_SECRET || 'dev-session-secret-change-me',
|
||||
getSecret: () => process.env.CSRF_SECRET || 'dev-csrf-secret-change-in-prod',
|
||||
```
|
||||
|
||||
Если переменные не заданы в проде — приложение молча стартует со слабыми предсказуемыми
|
||||
секретами (A02/A05). Подделка сессионных cookie/CSRF становится тривиальной.
|
||||
|
||||
**Рекомендация:** при `NODE_ENV === 'production'` — fail-fast, если секреты не заданы/равны дефолту.
|
||||
|
||||
### 1.8 ⚠️ CSP отключён
|
||||
|
||||
```js
|
||||
app.use(helmet({ contentSecurityPolicy: false }));
|
||||
```
|
||||
|
||||
Отключённая CSP убирает важный слой защиты от XSS (A03). EJS-шаблоны в промпте не приведены —
|
||||
**нельзя подтвердить**, что пользовательский ввод (`comment`, `companyName`, сообщения `error`/`message`
|
||||
из query) экранируется через `<%= %>`, а не `<%- %>`. `error`/`message` берутся прямо из `req.query`
|
||||
и рендерятся — при `<%- %>` это reflected XSS.
|
||||
|
||||
**Рекомендация:** включить разумную CSP; проверить, что все вывод-точки используют экранирование `<%= %>`.
|
||||
|
||||
### 1.9 ⚠️ `dev-login` — риск в проде
|
||||
|
||||
`/dev-login` при `DEV_MODE=true` (или заданном `DEV_SECRET`) позволяет войти под любым
|
||||
пользователем, включая `isAdmin: on`, без пароля. Если `DEV_MODE` случайно окажется `true` в проде —
|
||||
полный обход аутентификации.
|
||||
|
||||
**Рекомендация:** жёстко запретить `DEV_MODE` при `NODE_ENV=production` (отказ старта),
|
||||
а не полагаться на конфигурацию окружения.
|
||||
|
||||
### 1.10 ⚠️ Нет rate-limit на логин
|
||||
|
||||
POST `/login`, `/dev-login`, `/callback` не покрыты лимитером — для mock некритично,
|
||||
но при реальном OIDC `/callback` без лимита может использоваться для нагрузки на токен-эндпоинт KC.
|
||||
|
||||
### 1.11 ✅ Что сделано хорошо
|
||||
|
||||
- **SQL injection** — все запросы параметризованы (`$1, $2, ...`), конкатенации пользовательского
|
||||
ввода в SQL нет. ✅
|
||||
- **IDOR / изоляция компаний** — обычный пользователь не может передать `company_id`; для него всегда
|
||||
`getOrCreateCompany(clientId, ...)` по его собственному `clientId` из токена. Все мутации (`createEntry`,
|
||||
`updateEntry`, `deleteEntry`) фильтруют по `company_id`, а `getCompanyById` доступен только в админ-ветке. ✅
|
||||
- **CSRF-обработчик** ошибок (`EBADCSRFTOKEN`) даёт понятный 403. ✅
|
||||
- Cookie-флаги `httpOnly`, `secure` (в проде), `sameSite: 'lax'`. ✅
|
||||
|
||||
---
|
||||
|
||||
## 2. Корректность бизнес-логики, транзакции, конкурентность
|
||||
|
||||
### 2.1 ✅ Гонки при добавлении/лимиты
|
||||
|
||||
`createEntry` берёт `SELECT ... FOR UPDATE` по строке компании, затем считает count и
|
||||
проверяет пересечения внутри одной транзакции. Это сериализует параллельные вставки в рамках
|
||||
одной компании. Плюс частичный уникальный индекс `uq_entries_active_cidr` страхует от дублей
|
||||
на уровне БД. Хорошая многоуровневая защита. ✅
|
||||
|
||||
### 2.2 ⚠️ Проверка пересечений O(n) перебором в приложении
|
||||
|
||||
`createEntry`/`updateEntry` загружают все активные CIDR и сравнивают через `overlaps` в JS.
|
||||
При лимите ~15 записей это незаметно, но логика дублируется и проверка пересечений невозможна
|
||||
на уровне БД (индекс ловит только точный дубль, не overlap). Для текущих лимитов — приемлемо. ⚠️
|
||||
|
||||
### 2.3 ⚠️ `updateEntry`: `comment || old.comment`
|
||||
|
||||
Пустая строка комментария (`''`) трактуется как «не менять» и возвращает старый комментарий —
|
||||
пользователь не сможет очистить комментарий. Edge case. ⚠️
|
||||
|
||||
### 2.4 ⚠️ `value_cidr VARCHAR(18)` и regex в CHECK
|
||||
|
||||
Схема ограничивает `/\d{1,2}/` для маски, но приложение разрешает только `/22`–`/32` —
|
||||
согласовано. Однако CHECK-regex в БД допускает невалидные октеты (`999.999.999.999/40`),
|
||||
полагаясь полностью на валидацию приложения. Дубль-валидация на уровне БД неполная. ⚠️
|
||||
|
||||
### 2.5 ⚠️ `companyId` (UUID) из токена фактически не используется
|
||||
|
||||
Для обычного пользователя доступ к данным идёт по `companies.id` (SERIAL), полученному из
|
||||
`getOrCreateCompany(clientId)`. UUID `company_id` из токена в выборках не участвует. Это не баг
|
||||
(изоляция по `clientId` корректна), но источник путаницы: два разных идентификатора компании. ⚠️
|
||||
|
||||
### 2.6 ✅ Аудит в той же транзакции
|
||||
|
||||
`logAudit(..., client)` выполняется внутри транзакции мутации — запись аудита атомарна
|
||||
с изменением. ✅
|
||||
|
||||
---
|
||||
|
||||
## 3. CIDR-валидация и агрегация (`validators.js`)
|
||||
|
||||
### 3.1 ✅ `validate()`
|
||||
|
||||
- IPv6 отбрасывается, проверка формата, нормализация к адресу сети, проверка против
|
||||
`BLOCKED_RANGES`. Логика корректна для /22–/32.
|
||||
- Битовые операции `(acc << 8) + parseInt(...)` дают знаковое 32-битное промежуточное значение,
|
||||
но финальный `>>> 0` приводит к беззнаковому — для рассматриваемых масок результат верный. ✅
|
||||
|
||||
### 3.2 ⚠️ `overlaps()` — корректно, но нечитаемо
|
||||
|
||||
```js
|
||||
return a.start <= b.end && b.start <= a.start ||
|
||||
b.start <= a.end && a.start <= b.start;
|
||||
```
|
||||
|
||||
Сводится к «начало одного интервала лежит внутри другого» — это **корректный** критерий
|
||||
пересечения двух интервалов (проверено на граничных случаях: вложенность, смежность, непересечение).
|
||||
Но запись через смешанные `&&`/`||` без скобок хрупкая и трудна для ревью.
|
||||
|
||||
**Рекомендация:** заменить на каноническое `a.start <= b.end && b.start <= a.end`.
|
||||
|
||||
### 3.3 ⚠️ Список `BLOCKED_RANGES` неполон
|
||||
|
||||
Заблокированы RFC1918/CGNAT/loopback/link-local/multicast/reserved, но **не** `0.0.0.0/8`
|
||||
(«this network»). Можно добавить, например, `0.0.0.0/22`. Маловажно, но для строгого whitelist стоит закрыть.
|
||||
|
||||
### 3.4 ✅ `aggregateCIDRs()` / `rangeToCIDRs()`
|
||||
|
||||
- Сортировка по `start`, слияние перекрывающихся и **смежных** диапазонов (с защитой от переполнения
|
||||
`last.end < 0xFFFFFFFF`), затем разбиение объединённого диапазона на минимальный набор выровненных CIDR.
|
||||
- `rangeToCIDRs` корректно выбирает наибольший выровненный блок (`trailingZeros`) и уменьшает префикс,
|
||||
пока блок не помещается в диапазон; курсор всегда продвигается → бесконечного цикла нет, граница
|
||||
`0xFFFFFFFF` обработана. ✅
|
||||
|
||||
Алгоритмически — самая сильная часть проекта.
|
||||
|
||||
---
|
||||
|
||||
## 4. Архитектура и качество кода
|
||||
|
||||
### 4.1 ✅ Сильные стороны
|
||||
|
||||
- **Фабрики роутеров с DI** (`createRouter({...})`) — тестируемо, явные зависимости, без скрытых импортов состояния.
|
||||
- **Разделение слоёв**: `db` / `queries` / `validators` / `routes` / `middleware`.
|
||||
- **Транзакции** с корректным `BEGIN/COMMIT/ROLLBACK` и `finally { client.release() }`.
|
||||
- **Auth-абстракция** поддерживает и mock-RS256, и реальный OIDC за единым интерфейсом.
|
||||
|
||||
### 4.2 ⚠️ Замечания
|
||||
|
||||
- **Дублирование** обработки `company_id` в трёх хендлерах `entries.js` (add/edit/delete) — почти
|
||||
идентичный блок «определить компанию». Можно вынести в helper-middleware `resolveCompany`.
|
||||
- **Обработка ошибок через redirect c `error` в query** удобна для UI, но смешивает 4xx-валидацию
|
||||
и 5xx-сбои БД (любая ошибка `createEntry` уезжает в `?error=...`). Стоит различать пользовательские
|
||||
ошибки и системные (логировать stack для последних).
|
||||
- `cachedJwks` / `mockKeyPair` — модульное состояние; для горизонтального масштабирования mock-JWKS
|
||||
у каждого инстанса свой ключ → токены не валидны между подами. Для mock-режима ок, но в проде
|
||||
mock использоваться не должен.
|
||||
- **`express-session` MemoryStore** (стор не задан) — утечки памяти и потеря сессий при рестарте/масштабировании.
|
||||
Для прода нужен внешний стор (Redis/PG). ⚠️ (фактически блокер прода)
|
||||
- Комментарии-TODO прямо в коде (`csrf.js`, `db.js`) — хорошо, что зафиксированы, но это незакрытый долг.
|
||||
|
||||
---
|
||||
|
||||
## 5. Тесты
|
||||
|
||||
Из промпта видно ~50 юнит-тестов без БД: загрузка модулей, `config`, `validators` (30+ кейсов),
|
||||
`auth` (session middleware, `requireAdmin`).
|
||||
|
||||
### ✅ Покрыто
|
||||
- Валидаторы CIDR / агрегация / overlaps — основной риск-домен покрыт хорошо.
|
||||
- Session-middleware happy path и `requireAdmin`.
|
||||
|
||||
### ❌ Не покрыто (критично добавить)
|
||||
1. **Публичность `/export`** — тест, что неаутентифицированный запрос **не** получает данные
|
||||
(после фикса 1.1). Сейчас регрессия не отлавливается.
|
||||
2. **CSRF** — отклонение запроса без/с чужим токеном; привязка токена к сессии (фикс 1.2).
|
||||
3. **IDOR** — обычный пользователь пытается передать `company_id` чужой компании в add/edit/delete →
|
||||
должен работать только со своей.
|
||||
4. **Изоляция в `queries`** — пользователь A не видит/не меняет записи компании B.
|
||||
5. **Session fixation** — id сессии меняется после логина (фикс 1.3).
|
||||
6. **Open redirect** — `returnTo=//evil.com` не приводит к внешнему редиректу (фикс 1.4).
|
||||
7. **Лимиты/гонки** — параллельные `createEntry` не превышают лимит (интеграционный, с БД).
|
||||
8. **OIDC verify** — отклонение токена с чужим `iss`/`aud`, выбор ключа по `kid` (фикс 1.5).
|
||||
|
||||
Сейчас нет интеграционных тестов с БД и HTTP-слоем — основной пробел.
|
||||
|
||||
---
|
||||
|
||||
## 6. Готовность к продакшену — чек-лист блокеров
|
||||
|
||||
- [ ] ❌ Закрыть `/export` аутентификацией + фильтрацией по компании.
|
||||
- [ ] ❌ Починить CSRF `getSessionIdentifier` (`req.session.id`).
|
||||
- [ ] ❌ `session.regenerate()` при логине (fixation).
|
||||
- [ ] ❌ Валидация `returnTo` (open redirect).
|
||||
- [ ] ❌ OIDC: `issuer`/`audience`/`kid` + рефреш JWKS.
|
||||
- [ ] ❌ Внешний session store (Redis/PG) вместо MemoryStore.
|
||||
- [ ] ⚠️ TLS к БД с проверкой CA (`verify-full`).
|
||||
- [ ] ⚠️ Fail-fast при дефолтных секретах в проде.
|
||||
- [ ] ⚠️ Запретить `DEV_MODE`/`/dev-login` в проде.
|
||||
- [ ] ⚠️ Включить CSP; подтвердить экранирование EJS (`<%= %>`).
|
||||
- [ ] ⚠️ Добавить интеграционные тесты (export/CSRF/IDOR/fixation/OIDC).
|
||||
|
||||
---
|
||||
|
||||
## 7. Итоговая оценка по блокам
|
||||
|
||||
| Блок | Оценка |
|
||||
|------|--------|
|
||||
| SQL injection / параметризация | ✅ |
|
||||
| Изоляция компаний (IDOR в роутах) | ✅ (но без тестов) |
|
||||
| `/export` доступ | ❌ |
|
||||
| CSRF-конфигурация | ❌ |
|
||||
| Session-управление (fixation, store) | ❌ |
|
||||
| OIDC / token verification | ❌ |
|
||||
| Open redirect | ❌ |
|
||||
| TLS к БД / секреты | ⚠️ |
|
||||
| CSP / XSS (не подтверждено по views) | ⚠️ |
|
||||
| Бизнес-логика / транзакции / гонки | ✅ |
|
||||
| CIDR-валидация и агрегация | ✅ |
|
||||
| Архитектура / DI | ✅ |
|
||||
| Покрытие тестами | ⚠️ |
|
||||
|
||||
**Вывод:** ядро (валидация, агрегация, транзакции, изоляция в запросах) сделано грамотно.
|
||||
Блокируют прод в первую очередь четыре вещи: публичный `/export`, сломанная привязка CSRF,
|
||||
session fixation + MemoryStore и неполная проверка OIDC-токенов. После их устранения и добавления
|
||||
интеграционных тестов проект можно выводить в эксплуатацию.
|
||||
@@ -1,11 +0,0 @@
|
||||
<hr/>
|
||||
test
|
||||
<hr/>
|
||||
<cfflush/>
|
||||
<!--- <cftry>
|
||||
<cfdump var=#session#/>
|
||||
<cfcatch type="any"><cfdump var=#cfcatch#/></cfcatch>
|
||||
</cftry> --->
|
||||
|
||||
<cfmodule template="mod/crm_contract.cfm" wz="WZ01348"/>
|
||||
<cfdump var=#qComponent#/>
|
||||
@@ -1,522 +0,0 @@
|
||||
<cfcomponent
|
||||
displayname="Application"
|
||||
output="true"
|
||||
hint="Handle the application.">
|
||||
|
||||
|
||||
<!--- Pseudoconstructor --->
|
||||
<!--- Set up the application. --->
|
||||
<cfset this.Name = "PAYG-REPORT" />
|
||||
<cfset this.applicationTimeout = createTimeSpan( 0, 3, 0, 0 ) />
|
||||
<cfset this.sessionmanagement="Yes"/>
|
||||
<cfset this.clientmanagement="No"/>
|
||||
<cfset this.sessiontimeout=CreateTimeSpan(0, 1, 0, 0)/>
|
||||
<cfset this.setclientcookies="No"/>
|
||||
<cfset this.mappings = structNew() />
|
||||
<cfset this.mappings["/mod"] = getDirectoryFromPath(getCurrentTemplatePath()) & "mod/" />
|
||||
<cfset this.customTagPaths = expandPath(getDirectoryFromPath(getCurrentTemplatePath()) & "mod")/>
|
||||
<!--- <cfset this.nullSupport = true/> --->
|
||||
|
||||
<!--- SSO --->
|
||||
<cfset this.client_id = "payg-report.deck.nubes.ru"/>
|
||||
<cfset this.auth_endpoint = "https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/auth" />
|
||||
<cfset this.logout_endpoint = "https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/logout" />
|
||||
<cfset this.access_token_endpoint = "https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/token" />
|
||||
<cfset this.idpCertUrl = "https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/certs" />
|
||||
<cfset this.client_secret = createObject("java", "java.lang.System").getEnv("IDP_CLIENT_SECRET")/>
|
||||
<cfset this.iam_url = "https://auth-api.ngcloud.ru/api/v1/auth/user"/>
|
||||
<cfset this.redirect_uri = "https://#CGI.SERVER_NAME##CGI.SCRIPT_NAME#"/><!--- куда возвращаться от IDP --->
|
||||
|
||||
<cfset this.datasource = "rpt"/><!--- к буферной базе --->
|
||||
<cfset this.defaultdatasource = this.datasource/>
|
||||
<!--- request scope is available in pseudoconstructor
|
||||
see also https://www.bennadel.com/blog/1437-coldfusion-scope-existence-during-various-request-types-and-events.htm --->
|
||||
<cfset request.DS = "#this.datasource#"/>
|
||||
<cfset request.language=""/>
|
||||
<cfset getDS(this.datasource)/>
|
||||
<!--- кажется, нужно инициализировать датасорцы в псевдоконструкторе - onRequest не получается --->
|
||||
<!--- *** проверить, что есть - если есть, не создавать --->
|
||||
|
||||
|
||||
|
||||
<cffunction
|
||||
name="OnApplicationStart"
|
||||
access="public"
|
||||
returntype="boolean"
|
||||
output="false"
|
||||
hint="Fires when the application is first created.">
|
||||
|
||||
<!--- *** мы не проверяем срок жизни сертификата и его отзыв--->
|
||||
<!--- Obtain idp certificate OnApplicationStart --->
|
||||
<cfset application.idpCertificate = getIdpCertificate(this.idpCertUrl)/>
|
||||
<cfreturn true />
|
||||
</cffunction>
|
||||
|
||||
|
||||
<cffunction
|
||||
name="OnRequest"
|
||||
access="public"
|
||||
returntype="void"
|
||||
output="true"
|
||||
hint="Fires after pre page processing is complete.">
|
||||
|
||||
<cfargument name="template" type="string" required="true"/>
|
||||
|
||||
<cfset request.startTickCount=getTickCount()/>
|
||||
|
||||
<cfset setEncoding("FORM", "UTF-8")>
|
||||
<cfset setEncoding("URL", "UTF-8")>
|
||||
|
||||
<cfset request.PERMISSION_NONE=0/>
|
||||
<cfset request.PERMISSION_READ=1/>
|
||||
<cfset request.PERMISSION_WRITE=2/>
|
||||
|
||||
<!--- <cfset request.UNDEFINED_USR_ID=-1/>
|
||||
<cfset request.ANONYMOUS_USR_ID=2/>
|
||||
<cfset request.GUEST_USR_ID=3/> --->
|
||||
|
||||
<!--- global settings --->
|
||||
<cfset request.RECORDS_PER_PAGE=500/>
|
||||
<cfset request.APP_VERSION="0.00.020"/>
|
||||
<cfheader name="X-Application-Version" value=#request.APP_VERSION#/>
|
||||
<cfset request.STAND=getStand()/>
|
||||
|
||||
<!--- application constants --->
|
||||
|
||||
<!---<cfset request.SMTP_SERVER="172.16.16.16"/>
|
||||
<cfset request.MAIL_FROM_FOR_NOTIFICATION="Pipeline tracker <pipeline@mail.ru>"/>
|
||||
<cfset request.SUBJECT_PREFIX="[PIPELINE TRACKER] "/>--->
|
||||
<!---<cfset request.BASE_URL="https://smtest.dtln.local/pipeline/"/>*** сделать нормально. Внимание! При наличии прокси CGI.SERVER_NAME может быть заменено --->
|
||||
|
||||
<cfset local = {} />
|
||||
|
||||
<cfset local.basePath = getDirectoryFromPath(
|
||||
getCurrentTemplatePath()
|
||||
) />
|
||||
|
||||
<cfset local.targetPath = getDirectoryFromPath(
|
||||
expandPath( arguments.template )
|
||||
) />
|
||||
|
||||
<cfset local.requestDepth = (
|
||||
listLen( local.targetPath, "\/" ) -
|
||||
listLen( local.basePath, "\/" )
|
||||
) />
|
||||
|
||||
<cfset request.webRoot = repeatString(
|
||||
"../",
|
||||
local.requestDepth
|
||||
) />
|
||||
|
||||
<!---
|
||||
While we wouldn't normally do this for every page
|
||||
request (it would normally be cached in the
|
||||
application initialization), I'm going to calculate
|
||||
the site URL based on the web root.
|
||||
--->
|
||||
<cfset request.siteUrl = (
|
||||
IIF(
|
||||
(CGI.server_port_secure <!--- CGI.https EQ "On" does not work with apache+tomcat and nginx+tomcat --->),
|
||||
DE( "https://" ),
|
||||
DE( "http://" )
|
||||
) &
|
||||
cgi.http_host &
|
||||
reReplace(
|
||||
getDirectoryFromPath( arguments.template ), "([^\\/]+[\\/]){#local.requestDepth#}$",
|
||||
"",
|
||||
"one"
|
||||
)
|
||||
) />
|
||||
|
||||
<cfset request.thisPage=Replace(ReplaceNoCase(expandPath(ARGUMENTS.template), local.basePath, ""), "\", "/")/>
|
||||
<!--- <cfset request.thisUrl="#request.thisPage##len(CGI.queryString)#"/> чревато циклом --->
|
||||
|
||||
<!--- получение сертификата может занимать длительное время.
|
||||
Блокировать на это время приложение или сессию не следует.
|
||||
Вообще не следует ничего блокировать на время сетевого взаимодействия
|
||||
Нижеследующая, на первый взгляд, странная конструкция ровно для этого придумана
|
||||
--->
|
||||
<!--- <cfoutput>onRequest:check for cert started #(getTickCount()-request.startTickCount)#</cfoutput> --->
|
||||
<cfset var idpCertificateExists = false/>
|
||||
<cflock scope="application" type="readonly" timeout=1>
|
||||
<cfset request.APP_NAME=this.Name/>
|
||||
<cfset idpCertificateExists = structKeyExists(application,"idpCertificate")/>
|
||||
</cflock>
|
||||
|
||||
<cfif NOT idpCertificateExists>
|
||||
<!--- Obtain IDP Certificate (onRequest) --->
|
||||
<cfset var idpCertificate = getIdpCertificate(this.idpCertUrl) />
|
||||
<cflock scope="application" type="exclusive" timeout=1>
|
||||
<cfset application.idpCertificate = idpCertificate/>
|
||||
</cflock>
|
||||
</cfif>
|
||||
<!--- <cfoutput>onRequest:check for cert finished #(getTickCount()-request.startTickCount)#</cfoutput> --->
|
||||
|
||||
|
||||
|
||||
|
||||
<cfif structKeyExists(url,"logout")>
|
||||
<cflock scope="session" type="exclusive" timeout="3">
|
||||
<cfset structDelete(session, "auth")/>
|
||||
</cflock>
|
||||
</cfif>
|
||||
|
||||
<cfcookie name="CFID" value="#session.CFID#"/>
|
||||
<cfcookie name="CFTOKEN" value="#session.CFTOKEN#"/>
|
||||
|
||||
<cfinclude template="inc/functions.cfm"/>
|
||||
|
||||
<!--- SSO --->
|
||||
<!--- Если без сессии. Проверяем наличие токена в заголовке (и в куках?)
|
||||
Если его нет, идем к IDP за токеном. Получив токен, складываем его в куки
|
||||
Если токен есть, распаковываем его. Проверяем протухание. Если прошла установленная часть времени жизни, идем за рефрешем (внимание: это сработает только в браузере,
|
||||
если клиент не умеет редиректы - то токен должен быть достаточно свежий)
|
||||
(для начала делаем без рефреша, только проверяем протухание)
|
||||
А рефреш можно сделать на серверной стороне?
|
||||
Кладем данные из токена в реквест и наслаждаемся --->
|
||||
<!--- Вариант: если есть Authorization:bearer, идем с ним к IAM --->
|
||||
|
||||
<cfscript>
|
||||
|
||||
var requestData = GetHttpRequestData();
|
||||
var headers = requestData.headers;
|
||||
|
||||
if (structKeyExists(headers,"authorization")) { // аутентификация для клиента ЛК
|
||||
var authorizationHeader = headers.authorization;
|
||||
if (authorizationHeader.startsWith("Bearer ")) {
|
||||
var bearerToken = Mid(authorizationHeader, 8);
|
||||
writeDump(bearerToken);
|
||||
//var token=toString(binaryDecode(bearerToken, "base64"));
|
||||
//writeDump(token);
|
||||
//jwt = new lib.jwt(this.client_secret);
|
||||
//lock scope="application" type="readonly" timeout="1" {
|
||||
// var token_data = (jwt.decode(token, application.idpCertificate,"RS256"));
|
||||
//}
|
||||
var result="";
|
||||
//try {
|
||||
var iamService = new http(method = "GET", charset = "utf-8", url = #this.iam_url#, timeout="5");
|
||||
iamService.addParam(type = "HEADER", name = "Accept", value = "application/json");
|
||||
iamService.addParam(type = "HEADER", name = "Authorization", value = "#authorizationHeader#"); //passthrough
|
||||
//writedump(this.iamServiceUrl);abort;
|
||||
var resp = iamService.send();
|
||||
//if (resp.status_code NEQ 200) throw("IDP response not OK");
|
||||
//writedump(resp);abort;
|
||||
var prefix = resp.getPrefix();
|
||||
if (prefix.status_code NEQ 200) {
|
||||
//writedump(this.iamServiceUrl);
|
||||
//writedump(resp);
|
||||
var iamStatusCode = (isValid("integer", prefix.status_code)) ? val(prefix.status_code) : 500;
|
||||
throw(message="IAM error", detail="URL: #this.iam_url# Status Code: #prefix.status_code#");
|
||||
//abort;
|
||||
//throw("IDP response not OK");
|
||||
}
|
||||
result = prefix.filecontent;
|
||||
var idpUserData=deserializeJson(result);
|
||||
//request.auth.wz=idpUserData.userInfo.ClientId;//GUID!
|
||||
|
||||
|
||||
request.auth.login = idpUserData.userInfo.login;
|
||||
//request.usrUid=idpUserData.userInfo.contactId;//userId; //GUID!
|
||||
//writedump(idpUserData);abort;
|
||||
//} catch (e) {
|
||||
/*if (fileExists("#GetDirectoryFromPath(GetCurrentTemplatePath())#/etc/local-debug")) { //true for local debug without IDP, etc/* does not go to repository
|
||||
var result='{"accounts":[{"login":"","type":"telegram"}],"avatar":["d2d2b7ac-50af-432b-b7c4-f7d6561e288a"],"company":"ООО «НУБЕС»","companyId":"8ec70ac0-546d-42a7-8cff-339c8fb51a23","contactId":"983967a3-58c2-4cdd-84a5-8b427ccfac82","email":"smishchuk@nubes.ru","externalUser":false,"fio":{"fullName":"Мищук Сергей ","name":"Сергей","secondName":"","surname":"Мищук"},"groupIds":["94bf5be7-52f4-5c13-87c3-513786934685","ab12b6b8-0265-4683-a5c2-5e1a74a55216","aff008c3-7443-50f6-86e1-ecff3cd3b04d","d6000da0-c9aa-55eb-9882-f118b432730b","d89a33dc-3177-5854-9580-f7f860a5ab7c","ddfe2555-9ef4-42cb-9a2b-7f24e61e7747","df12926f-ecb8-5479-857f-6d291464baad","fda5c295-230a-5025-9797-b8b4e99e08aa","2be9b4b6-94d1-59f9-a649-cab228d82169","64fe6f5c-a91b-5fea-a7ca-d9823701ebd4"],"integration":{"serviceId":""},"login":"smishchuk@nubes.ru","mobilePhone":[],"position":"","userId":"d72530e1-66a4-412f-a046-38153c4e5405"}';}
|
||||
*/
|
||||
//writedump(resp);
|
||||
//return representationOf( {"exception"=e} );
|
||||
//rethrow(e);
|
||||
//}
|
||||
}
|
||||
} else {
|
||||
//точно мы так не словим дедлок ВПР
|
||||
lock scope="session" type="readonly" timeout="3" {
|
||||
if (structKeyExists(session, "auth")) {
|
||||
request.auth = structCopy(session.auth);
|
||||
}
|
||||
}
|
||||
if (structKeyExists(request, "auth") AND Now() < request.auth.exp) {
|
||||
|
||||
if (dateDiff("s", request.auth.iat, Now()) > request.auth.refresh_expires_in/1.5 AND CGI.REQUEST_METHOD EQ "GET") {
|
||||
// доля времени, после которой происходит рефреш, прибита гвоздями
|
||||
// По всей видимости, рефреш будет ломать обращения POSТ
|
||||
// В связи с чем предлагается дождаться GET
|
||||
// Приклеивать query_string как-то не пришлось, браузер сам справляется
|
||||
|
||||
// refresh
|
||||
var idp = new lib.oauth2(this.client_id, this.client_secret, this.auth_endpoint, this.access_token_endpoint, this.redirect_uri);
|
||||
var resp = idp.refreshAccessTokenRequest(request.auth.refresh_token);
|
||||
|
||||
lock scope="application" type="readonly" timeout="1" {
|
||||
idpCertificate = application.idpCertificate;
|
||||
}
|
||||
|
||||
var auth = parseIdpresponse(resp, idpCertificate, this.client_secret); //все эти сложности из-за опасений насчет многопоточности и блокировок
|
||||
|
||||
lock scope="session" type="exclusive" timeout="1" {
|
||||
session.auth = auth;
|
||||
}
|
||||
request.auth = auth;
|
||||
writeDump(auth);
|
||||
}
|
||||
} else {
|
||||
// сессии SSO нет
|
||||
// или токен протух
|
||||
request.auth_state = createGUID().toString();
|
||||
lock scope="session" type="exclusive" timeout="1" {
|
||||
structDelete(session, "auth"); // зачистили сессию от данных SSO
|
||||
session.auth_state = request.auth_state;
|
||||
}
|
||||
|
||||
var idp = new lib.oauth2(this.client_id, this.client_secret, this.auth_endpoint, this.access_token_endpoint, this.redirect_uri);
|
||||
/*
|
||||
часто появляется ошибка: после неудачного запроса к IDP
|
||||
в адресной строке браузера остается все, с чем его редиректил IDP,
|
||||
а повторный запрос с этим контентом уже не работает. Получаем 400 Bad Request -
|
||||
может быть, KeyCloak так защищается от циклических редиректов
|
||||
Не очень понятно, как предотвратить повторный запрос (можно, конечно, использовать сессию, но странно)
|
||||
*/
|
||||
//writeOutput(getTickCount()-request.startTickCount);
|
||||
if(structKeyExists(url, "code")) { //обрабатываем редирект от IDP *** наличие поля code в URL введет нас в заблуждение,
|
||||
|
||||
try {
|
||||
//writeOutput("idp.makeAccessTokenRequest");
|
||||
//writeOutput(getTickCount()-request.startTickCount);
|
||||
var resp = idp.makeAccessTokenRequest(url.code);
|
||||
|
||||
lock scope="application" type="readonly" timeout="1" {
|
||||
idpCertificate = application.idpCertificate;
|
||||
}
|
||||
|
||||
var auth = parseIdpresponse(resp, idpCertificate, this.client_secret); //все эти сложности из-за опасений насчет многопоточности и блокировок
|
||||
|
||||
lock scope="session" type="exclusive" timeout="1" {
|
||||
session.auth = auth;
|
||||
}
|
||||
//request.auth = auth;
|
||||
location(this.redirect_uri, false);
|
||||
} catch (e) {echo('<br>****************** #e.message# : #e.detail# **************');}
|
||||
|
||||
} else { //отправляем браузер к IDP
|
||||
var strURL =idp.buildRedirectToAuthURL({"scope":'openid profile email',"state":request.auth_state,"allow_signup":false});
|
||||
echo('<a href="#strURL#">self-made Auth link (idp) #strURL#</a> <br> <br>');
|
||||
location(strUrl,false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//request.logout_url = "";
|
||||
if (structKeyExists(request, "auth") AND structKeyExists(request.auth, "id_token")) {
|
||||
request.logout_url = "#this.logout_endpoint#?id_token_hint=#request.auth.id_token#&post_logout_redirect_uri=#this.redirect_uri#?logout";
|
||||
// внимание, здесь подразумевается, что redirect_uri не содержит query_string
|
||||
// после возвращения от IDP нам нужно будет зачистить сессию SSO, сделаем это по слову logout
|
||||
}
|
||||
|
||||
request.auth.wz='WZ01348'; //*******************
|
||||
</cfscript>
|
||||
|
||||
|
||||
<cfif structKeyExists(request, "auth")>
|
||||
<cfinclude template="#ARGUMENTS.template#"/>
|
||||
<cfelse>
|
||||
<cfset currentDir = (
|
||||
lCase(
|
||||
replace(
|
||||
getDirectoryFromPath(
|
||||
replaceNoCase(
|
||||
expandPath(
|
||||
ARGUMENTS.template
|
||||
),
|
||||
getDirectoryFromPath(
|
||||
getCurrentTemplatePath()
|
||||
),
|
||||
""
|
||||
)
|
||||
), "\", "/", "ALL"
|
||||
)
|
||||
)
|
||||
)/>
|
||||
<!--- <cfif currentDir EQ "saml/"><!--- note traling slash --->
|
||||
<cfinclude template="#ARGUMENTS.template#"/>
|
||||
<cfelse>
|
||||
<cfinclude template="login.cfm" />
|
||||
</cfif> --->
|
||||
</cfif>
|
||||
|
||||
|
||||
|
||||
<cfreturn />
|
||||
</cffunction>
|
||||
|
||||
|
||||
<cffunction
|
||||
name="OnRequestEnd"
|
||||
access="public"
|
||||
returntype="void"
|
||||
output="true"
|
||||
hint="Fires after the page processing is complete.">
|
||||
<!--- Attention! Before CF9, OnrequestEnd is not executed in case of redirect. That is why we use session.save_login --->
|
||||
|
||||
<cfif structKeyExists(session, "save_login")>
|
||||
<cfif session.save_login EQ "">
|
||||
<!--- unset (expire) cookie --->
|
||||
<cfcookie expires="-1" name="portalUser" value="">
|
||||
<cfelse>
|
||||
<!--- set persistent cookie with no expiration --->
|
||||
<cfcookie expires="NEVER" name="portalUser" value="#encrypt(session.save_login,COOKIEENCKEY)#">
|
||||
</cfif>
|
||||
<cfset structDelete(session, "save_login")>
|
||||
</cfif>
|
||||
|
||||
<cfreturn />
|
||||
</cffunction>
|
||||
|
||||
<cffunction
|
||||
name="getStand"
|
||||
access="private"
|
||||
returntype="string"
|
||||
output="true"><!--- *** duplicated in svc-api --->
|
||||
|
||||
<cftry>
|
||||
<cfquery name=qConfig>
|
||||
select value as stand from config
|
||||
where name='STAND'
|
||||
</cfquery>
|
||||
<cfreturn qConfig.stand/>
|
||||
|
||||
<cfcatch type="ANY">
|
||||
<!--- do nothing, default will be returned --->
|
||||
</cfcatch>
|
||||
</cftry>
|
||||
<cfreturn ""/>
|
||||
</cffunction>
|
||||
|
||||
|
||||
<cffunction
|
||||
name="getDS"
|
||||
access="private"
|
||||
returntype="void"
|
||||
output="true"
|
||||
hint="Configure data source from environment variables (if datasource with the name provided already exists, does nothing). Convention: data source name is an environment varialble prefix">
|
||||
|
||||
<cfargument name="dsname" type="string" required="true"/>
|
||||
<cfargument name="prefix" type="string" default=#dsname#/>
|
||||
|
||||
<cftry>
|
||||
<cfquery name="qTestDs" datasource=#arguments.dsname#>
|
||||
select 1;
|
||||
</cfquery>
|
||||
|
||||
<cfcatch type="any">
|
||||
<cfset var ds={}/>
|
||||
<cfset var system = createObject("java", "java.lang.System")/>
|
||||
|
||||
<cfloop list="class,connectionString,database,driver,dbdriver,host,port,type,url,username,password,bundleName,bundleVersion,connectionLimit,liveTimeout,validate" item="field"><!--- driver vs dbdriver --->
|
||||
<cfset var value=system.getEnv("#arguments.prefix#_#field#")/>
|
||||
<cfif isDefined("value") AND len(value)>
|
||||
<cfset structInsert(ds,field,value)/>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
<cfif structIsEmpty(ds)>
|
||||
<cfthrow type="application" message="Datasource not configured" detail="Datasource not defined in the environment. Expected prefix is #arguments.prefix#"/>
|
||||
</cfif>
|
||||
|
||||
<!--- test datasource (just to get exception if invalid) --->
|
||||
<cftry>
|
||||
<cfquery name="qTestDsNextTry" datasource=#ds#>
|
||||
select 2;
|
||||
</cfquery>
|
||||
|
||||
<cfcatch type="any">
|
||||
<!--- <cfdump var=#ds#/><cfabort/> --->
|
||||
<cfdump var=#arguments#/>
|
||||
<cfdump var=#ds#/>
|
||||
<cfrethrow/>
|
||||
</cfcatch>
|
||||
</cftry>
|
||||
|
||||
<cfset this.datasources["#arguments.dsname#"]=#ds#/> <!--- Интересно, доступен ли здесь this --->
|
||||
</cfcatch>
|
||||
</cftry>
|
||||
<!--- <cfdump var=#ds#/> --->
|
||||
<cfreturn/>
|
||||
</cffunction>
|
||||
|
||||
<cfscript>
|
||||
//https://keycloak.nubes.ru/admin
|
||||
//https://keycloak.nubes.ru/realms/SSH_CA/account //smishchuk@mgmt.nubes.ru
|
||||
//https://stackoverflow.com/questions/28658735/what-are-keycloaks-oauth2-openid-connect-endpoints
|
||||
//http://https://keycloak.nubes.ru/realms/SSH_CA/.well-known/openid-configuration
|
||||
|
||||
//https://keycloak.nubes.ru/realms/cloud/.well-known/openid-configuration тут ссылка
|
||||
//https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/certs тут серт
|
||||
//только надо его обрамить -----BEGIN CERTIFICATE-----
|
||||
private function getIdpCertificate(certUrl) {
|
||||
|
||||
var httpService = new http();
|
||||
httpService.setMethod( "get" );
|
||||
httpService.setCharset( "utf-8" );
|
||||
httpService.setUrl(arguments.certUrl);
|
||||
|
||||
var result = httpService.send().getPrefix();
|
||||
var status_code = result.ResponseHeader['Status_Code'];
|
||||
|
||||
if ('200' == status_code) {
|
||||
var content = deserializeJson(result.FileContent);
|
||||
//мы подразумеваем структуру конкретно KeyCloak
|
||||
var key = "";
|
||||
for (key in content.keys) {
|
||||
if ("RSA" == key.kty AND "sig" == key.use) {
|
||||
return "-----BEGIN CERTIFICATE-----" & key.x5c[1] & "-----END CERTIFICATE-----";
|
||||
}
|
||||
}
|
||||
throw(message="RSA cetificate not found");
|
||||
} else {
|
||||
throw (message="Cannot obtain IDP certificate, request failed", detail="Status_Code #status_code#");
|
||||
}
|
||||
}
|
||||
|
||||
private struct function parseIdpResponse(struct resp, string certificate, string jwt_secret) {
|
||||
|
||||
var auth = structNew("linked");
|
||||
var data = deserializeJson(arguments.resp.content);
|
||||
var jwt = new lib.jwt(arguments.jwt_secret);
|
||||
|
||||
auth.expires_in = data.expires_in;
|
||||
auth.refresh_expires_in = data.refresh_expires_in;
|
||||
auth.access_token = data.access_token;
|
||||
auth.refresh_token = data.refresh_token;
|
||||
auth.id_token = data.id_token;
|
||||
auth.token_type = data.token_type;
|
||||
|
||||
//idp_response_content = structCopy(data); //for debug
|
||||
var token_data = (jwt.decode(data.access_token, arguments.certificate,"RS256"));
|
||||
|
||||
auth.session_state = token_data.session_state;
|
||||
auth.sid = token_data.sid;
|
||||
auth.auth_time = token_data.auth_time;
|
||||
auth.exp = token_data.exp;
|
||||
auth.iat = token_data.iat;
|
||||
|
||||
auth.login = token_data.preferred_username;
|
||||
auth.wz = token_data.ClientID;
|
||||
auth.fullname = token_data.name;
|
||||
auth.groups = token_data.groups;
|
||||
//auth.token_data = structCopy(token_data); //for debug
|
||||
|
||||
return (auth);
|
||||
}
|
||||
|
||||
</cfscript>
|
||||
|
||||
<cffunction name="rethrow" returntype="void">
|
||||
<!--- https://www.raymondcamden.com/2004/03/09/3089633C-9FA0-606B-3F540AE9642A795F --->
|
||||
<cftry>
|
||||
<cfcatch>
|
||||
<cfrethrow/>
|
||||
</cfcatch>
|
||||
</cftry>
|
||||
<cfthrow type="Context validation error" message="RETHROW() called outside TRY-CATCH"/>
|
||||
</cffunction>
|
||||
</cfcomponent>
|
||||
@@ -1,6 +0,0 @@
|
||||
корректный период
|
||||
другие WZ
|
||||
? full join
|
||||
детализация - для S3 это бакеты
|
||||
|
||||
|
||||
@@ -1,595 +0,0 @@
|
||||
база rpt сделана из базы billing (название изменено, чтобы избежать путаницы)
|
||||
-- удалены последовательности, переназначены владельцы и права
|
||||
-- удалены внешние ключи
|
||||
|
||||
-- создать роль dbo (владелец), без connect
|
||||
-- создать пользователя nifi, с правом логина
|
||||
-- создать пользователя appserver, с правом логина
|
||||
-- дать права пользователю nifi на схемы и таблицы
|
||||
-- дать права пользователю appserver на схемы и таблицы на чтение
|
||||
|
||||
SET ROLE postgres;
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS s3billing;
|
||||
CREATE SCHEMA IF NOT EXISTS ngcloud_ru;
|
||||
|
||||
ALTER SCHEMA s3billing OWNER TO dbo;
|
||||
ALTER SCHEMA ngcloud_ru OWNER TO dbo;
|
||||
ALTER DATABASE rpt SET search_path TO s3billing, ngcloud_ru;
|
||||
|
||||
ALTER DEFAULT PRIVILEGES FOR ROLE dbo, postgres GRANT ALL ON TABLES TO nifi;
|
||||
ALTER DEFAULT PRIVILEGES FOR ROLE dbo, postgres GRANT SELECT ON TABLES TO appserver;
|
||||
|
||||
SET ROLE dbo;
|
||||
|
||||
GRANT USAGE ON SCHEMA s3billing TO appserver,nifi;
|
||||
GRANT USAGE ON SCHEMA ngcloud_ru TO appserver,nifi;
|
||||
|
||||
GRANT ALL ON ALL TABLES IN SCHEMA s3billing TO nifi;
|
||||
GRANT SELECT ON ALL TABLES IN SCHEMA s3billing TO appserver;
|
||||
|
||||
GRANT ALL ON ALL TABLES IN SCHEMA ngcloud_ru TO nifi;
|
||||
GRANT SELECT ON ALL TABLES IN SCHEMA ngcloud_ru TO appserver;
|
||||
|
||||
--GRANT ALL ON ALL SEQUENCES IN SCHEMA s3billing TO appserver,nifi;
|
||||
--GRANT ALL ON ALL SEQUENCES IN SCHEMA ngcloud_ru TO appserver,nifi;
|
||||
|
||||
|
||||
--ALTER DEFAULT PRIVILEGES FOR ROLE dbo GRANT ALL ON SEQUENCES TO appserver,nifi;
|
||||
|
||||
--
|
||||
-- PostgreSQL database dump
|
||||
--
|
||||
|
||||
-- Dumped from database version 16.0
|
||||
-- Dumped by pg_dump version 16.4
|
||||
|
||||
-- Started on 2025-09-15 09:50:58
|
||||
|
||||
SET statement_timeout = 0;
|
||||
SET lock_timeout = 0;
|
||||
SET idle_in_transaction_session_timeout = 0;
|
||||
SET client_encoding = 'UTF8';
|
||||
SET standard_conforming_strings = on;
|
||||
SELECT pg_catalog.set_config('search_path', '', false);
|
||||
SET check_function_bodies = false;
|
||||
SET xmloption = content;
|
||||
SET client_min_messages = warning;
|
||||
SET row_security = off;
|
||||
|
||||
SET default_tablespace = '';
|
||||
|
||||
SET default_table_access_method = heap;
|
||||
|
||||
--
|
||||
-- TOC entry 230 (class 1259 OID 2160668)
|
||||
-- Name: bucket_info; Type: TABLE; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS s3billing.bucket_info;
|
||||
CREATE TABLE s3billing.bucket_info (
|
||||
id integer NOT NULL,
|
||||
bucket_id character varying(255) NOT NULL,
|
||||
name character varying(255) NOT NULL,
|
||||
marker character varying(255) NOT NULL,
|
||||
placement_id bigint DEFAULT 0 NOT NULL
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 226 (class 1259 OID 2160642)
|
||||
-- Name: bucket_stat; Type: TABLE; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
DROP TABLE IF EXISTS s3billing.bucket_stat;
|
||||
CREATE TABLE s3billing.bucket_stat (
|
||||
bucket_id bigint,
|
||||
num_shards bigint NOT NULL,
|
||||
tenant character varying(255) DEFAULT NULL::character varying,
|
||||
zone_group bigint NOT NULL,
|
||||
placement_id bigint NOT NULL,
|
||||
explicit_placement_data_pool character varying(255) DEFAULT NULL::character varying,
|
||||
explicit_placement_data_extra_pool character varying(255) DEFAULT NULL::character varying,
|
||||
explicit_placement_index_pool character varying(255) DEFAULT NULL::character varying,
|
||||
index_type_id bigint NOT NULL,
|
||||
versioned boolean DEFAULT false,
|
||||
versioning_enabled boolean DEFAULT false,
|
||||
object_lock_enabled boolean DEFAULT false,
|
||||
mfa_enabled boolean DEFAULT false,
|
||||
owner character varying(255) NOT NULL,
|
||||
bucket_quota_enabled boolean DEFAULT false,
|
||||
bucket_quota_check_on_raw boolean DEFAULT false,
|
||||
bucket_quota_max_size bigint NOT NULL,
|
||||
bucket_quota_max_size_kb bigint NOT NULL,
|
||||
bucket_quota_max_objects bigint NOT NULL,
|
||||
usage_rgw_main_size bigint NOT NULL,
|
||||
usage_rgw_main_size_actual bigint NOT NULL,
|
||||
usage_rgw_main_size_utilized bigint NOT NULL,
|
||||
usage_rgw_main_size_kb bigint NOT NULL,
|
||||
usage_rgw_main_size_kb_actual bigint NOT NULL,
|
||||
usage_rgw_main_size_kb_utilized bigint NOT NULL,
|
||||
usage_rgw_main_num_objects bigint NOT NULL,
|
||||
usage_rgw_multimeta_size bigint,
|
||||
usage_rgw_multimeta_size_actual bigint,
|
||||
usage_rgw_multimeta_size_utilized bigint,
|
||||
usage_rgw_multimeta_size_kb bigint,
|
||||
usage_rgw_multimeta_size_kb_actual bigint,
|
||||
usage_rgw_multimeta_size_kb_utilized bigint,
|
||||
usage_rgw_multimeta_num_objects bigint,
|
||||
creation_time timestamp without time zone NOT NULL,
|
||||
mtime timestamp without time zone NOT NULL,
|
||||
timestamp_addition timestamp without time zone DEFAULT date_trunc('hour'::text, CURRENT_TIMESTAMP)
|
||||
);
|
||||
|
||||
--
|
||||
-- TOC entry 245 (class 1259 OID 2160763)
|
||||
-- Name: category; Type: TABLE; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
DROP TABLE IF EXISTS s3billing.category;
|
||||
CREATE TABLE s3billing.category (
|
||||
id integer NOT NULL,
|
||||
name character varying(255) NOT NULL
|
||||
);
|
||||
|
||||
--
|
||||
-- TOC entry 225 (class 1259 OID 2160635)
|
||||
-- Name: goose_db_version; Type: TABLE; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
DROP TABLE IF EXISTS s3billing.goose_db_version;
|
||||
CREATE TABLE s3billing.goose_db_version (
|
||||
id integer NOT NULL,
|
||||
version_id bigint NOT NULL,
|
||||
is_applied boolean NOT NULL,
|
||||
tstamp timestamp without time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 234 (class 1259 OID 2160684)
|
||||
-- Name: index_type; Type: TABLE; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
DROP TABLE IF EXISTS index_type;
|
||||
CREATE TABLE s3billing.index_type (
|
||||
id integer NOT NULL,
|
||||
name character varying(255) NOT NULL
|
||||
);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 232 (class 1259 OID 2160677)
|
||||
-- Name: placement; Type: TABLE; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
DROP TABLE IF EXISTS s3billing.placement;
|
||||
CREATE TABLE s3billing.placement (
|
||||
id integer NOT NULL,
|
||||
name character varying(255) NOT NULL
|
||||
);
|
||||
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 239 (class 1259 OID 2160705)
|
||||
-- Name: usage_bucket_by_user; Type: TABLE; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
DROP TABLE IF EXISTS s3billing.usage_bucket_by_user;
|
||||
CREATE TABLE s3billing.usage_bucket_by_user (
|
||||
bucketid bigint,
|
||||
"time" timestamp without time zone NOT NULL,
|
||||
bytes_sent bigint NOT NULL,
|
||||
bytes_received bigint NOT NULL,
|
||||
ops bigint NOT NULL,
|
||||
successful_ops bigint NOT NULL,
|
||||
date_start timestamp without time zone NOT NULL,
|
||||
date_end timestamp without time zone NOT NULL,
|
||||
user_id bigint NOT NULL,
|
||||
category_id bigint NOT NULL
|
||||
);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 238 (class 1259 OID 2160698)
|
||||
-- Name: usage_stat_summary; Type: TABLE; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
DROP TABLE IF EXISTS s3billing.usage_stat_summary;
|
||||
CREATE TABLE s3billing.usage_stat_summary (
|
||||
id integer NOT NULL,
|
||||
userid character varying(255) NOT NULL,
|
||||
category character varying(255) NOT NULL,
|
||||
bytes_sent bigint NOT NULL,
|
||||
bytes_received bigint NOT NULL,
|
||||
ops bigint NOT NULL,
|
||||
successful_ops bigint NOT NULL,
|
||||
date_start timestamp without time zone NOT NULL,
|
||||
date_end timestamp without time zone NOT NULL
|
||||
);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 236 (class 1259 OID 2160691)
|
||||
-- Name: usage_stat_total; Type: TABLE; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
DROP TABLE IF EXISTS s3billing.usage_stat_total;
|
||||
CREATE TABLE s3billing.usage_stat_total (
|
||||
id integer NOT NULL,
|
||||
userid character varying(255) NOT NULL,
|
||||
bytes_sent bigint NOT NULL,
|
||||
bytes_received bigint NOT NULL,
|
||||
ops bigint NOT NULL,
|
||||
successful_ops bigint NOT NULL,
|
||||
date_start timestamp without time zone NOT NULL,
|
||||
date_end timestamp without time zone NOT NULL
|
||||
);
|
||||
|
||||
--
|
||||
-- TOC entry 243 (class 1259 OID 2160751)
|
||||
-- Name: user_info; Type: TABLE; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
DROP TABLE IF EXISTS s3billing.user_info;
|
||||
CREATE TABLE s3billing.user_info (
|
||||
id integer NOT NULL,
|
||||
name character varying(255) NOT NULL,
|
||||
owner character varying(255) NOT NULL
|
||||
);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 241 (class 1259 OID 2160712)
|
||||
-- Name: user_quota_bucket; Type: TABLE; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
DROP TABLE IF EXISTS s3billing.user_quota_bucket;
|
||||
CREATE TABLE s3billing.user_quota_bucket (
|
||||
id integer NOT NULL,
|
||||
userid character varying(255) NOT NULL,
|
||||
enabled boolean DEFAULT false,
|
||||
check_on_raw boolean DEFAULT false,
|
||||
max_size bigint NOT NULL,
|
||||
max_size_kb bigint NOT NULL,
|
||||
max_objects bigint NOT NULL
|
||||
);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 228 (class 1259 OID 2160661)
|
||||
-- Name: zone_group; Type: TABLE; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
DROP TABLE IF EXISTS s3billing.zone_group;
|
||||
CREATE TABLE s3billing.zone_group (
|
||||
id integer NOT NULL,
|
||||
name uuid NOT NULL
|
||||
);
|
||||
|
||||
--
|
||||
-- TOC entry 3278 (class 2606 OID 2160675)
|
||||
-- Name: bucket_info bucket_info_pkey; Type: CONSTRAINT; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY s3billing.bucket_info
|
||||
ADD CONSTRAINT bucket_info_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3273 (class 2606 OID 2476484)
|
||||
-- Name: bucket_stat bucket_stat_unique; Type: CONSTRAINT; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
--ALTER TABLE ONLY s3billing.bucket_stat
|
||||
-- ADD CONSTRAINT bucket_stat_unique UNIQUE (bucket_id, placement_id, timestamp_addition);
|
||||
|
||||
ALTER TABLE ONLY s3billing.bucket_stat
|
||||
ADD CONSTRAINT bucket_stat_pkey PRIMARY KEY (bucket_id, placement_id, timestamp_addition);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3298 (class 2606 OID 2160768)
|
||||
-- Name: category category_pkey; Type: CONSTRAINT; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY s3billing.category
|
||||
ADD CONSTRAINT category_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3270 (class 2606 OID 2160640)
|
||||
-- Name: goose_db_version goose_db_version_pkey; Type: CONSTRAINT; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY s3billing.goose_db_version
|
||||
ADD CONSTRAINT goose_db_version_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3284 (class 2606 OID 2160689)
|
||||
-- Name: index_type index_type_pkey; Type: CONSTRAINT; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY s3billing.index_type
|
||||
ADD CONSTRAINT index_type_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3282 (class 2606 OID 2160682)
|
||||
-- Name: placement placement_pkey; Type: CONSTRAINT; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY s3billing.placement
|
||||
ADD CONSTRAINT placement_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3290 (class 2606 OID 2476705)
|
||||
-- Name: usage_bucket_by_user usage_bucket_by_user_unique; Type: CONSTRAINT; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
--ALTER TABLE ONLY s3billing.usage_bucket_by_user
|
||||
-- ADD CONSTRAINT usage_bucket_by_user_unique UNIQUE (bucketid, date_start, date_end, user_id, category_id);
|
||||
ALTER TABLE ONLY s3billing.usage_bucket_by_user
|
||||
ADD CONSTRAINT usage_bucket_by_user_pkey PRIMARY KEY (bucketid, date_start, date_end, user_id, category_id);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3288 (class 2606 OID 2160703)
|
||||
-- Name: usage_stat_summary usage_stat_summary_pkey; Type: CONSTRAINT; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY s3billing.usage_stat_summary
|
||||
ADD CONSTRAINT usage_stat_summary_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3286 (class 2606 OID 2160696)
|
||||
-- Name: usage_stat_total usage_stat_total_pkey; Type: CONSTRAINT; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY s3billing.usage_stat_total
|
||||
ADD CONSTRAINT usage_stat_total_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3295 (class 2606 OID 2160758)
|
||||
-- Name: user_info user_info_pkey; Type: CONSTRAINT; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY s3billing.user_info
|
||||
ADD CONSTRAINT user_info_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3292 (class 2606 OID 2160719)
|
||||
-- Name: user_quota_bucket user_quota_bucket_pkey; Type: CONSTRAINT; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY s3billing.user_quota_bucket
|
||||
ADD CONSTRAINT user_quota_bucket_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3275 (class 2606 OID 2160666)
|
||||
-- Name: zone_group zone_group_pkey; Type: CONSTRAINT; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY s3billing.zone_group
|
||||
ADD CONSTRAINT zone_group_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3276 (class 1259 OID 2474077)
|
||||
-- Name: bucket_info_name_idx; Type: INDEX; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
CREATE INDEX bucket_info_name_idx ON s3billing.bucket_info USING btree (name);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3279 (class 1259 OID 2474078)
|
||||
-- Name: bucket_info_placement_id_idx; Type: INDEX; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
CREATE INDEX bucket_info_placement_id_idx ON s3billing.bucket_info USING btree (placement_id);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3271 (class 1259 OID 2474094)
|
||||
-- Name: bucket_stat_owner_idx; Type: INDEX; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
CREATE INDEX bucket_stat_owner_idx ON s3billing.bucket_stat USING btree (owner);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3296 (class 1259 OID 2474445)
|
||||
-- Name: category_name_idx; Type: INDEX; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
CREATE UNIQUE INDEX category_name_idx ON s3billing.category USING btree (name);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3280 (class 1259 OID 2474468)
|
||||
-- Name: placement_name_idx; Type: INDEX; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
CREATE UNIQUE INDEX placement_name_idx ON s3billing.placement USING btree (name);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3293 (class 1259 OID 2474438)
|
||||
-- Name: user_info_name_idx; Type: INDEX; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
CREATE INDEX user_info_name_idx ON s3billing.user_info USING btree (name);
|
||||
|
||||
|
||||
|
||||
-- Completed on 2025-09-15 09:50:59
|
||||
|
||||
--
|
||||
-- PostgreSQL database dump complete
|
||||
--
|
||||
|
||||
-------------------------------------------------------------------------------------
|
||||
-------------------------------------------------------------------------------------
|
||||
-------------------------------------------------------------------------------------
|
||||
|
||||
DROP TABLE IF EXISTS ngcloud_ru.capacity_resource;
|
||||
CREATE TABLE ngcloud_ru.capacity_resource (
|
||||
vdc_id uuid NOT NULL,
|
||||
"timestamp" timestamp with time zone NOT NULL,
|
||||
cpu_allocated integer NOT NULL,
|
||||
cpu_limit integer NOT NULL,
|
||||
cpu_reserved integer NOT NULL,
|
||||
cpu_used integer NOT NULL,
|
||||
cpu_speed integer NOT NULL,
|
||||
mem_allocated integer NOT NULL,
|
||||
mem_limit integer NOT NULL,
|
||||
mem_reserved integer NOT NULL,
|
||||
mem_used integer NOT NULL
|
||||
);
|
||||
|
||||
--
|
||||
-- TOC entry 223 (class 1259 OID 1893993)
|
||||
-- Name: capacity_storage; Type: TABLE; Schema: ngcloud_ru; Owner: vc_billing
|
||||
--
|
||||
DROP TABLE IF EXISTS ngcloud_ru.capacity_storage;
|
||||
CREATE TABLE ngcloud_ru.capacity_storage (
|
||||
vdc_id uuid NOT NULL,
|
||||
storage_profile_types_id integer NOT NULL,
|
||||
"timestamp" timestamp with time zone NOT NULL,
|
||||
id uuid NOT NULL,
|
||||
"limit" integer NOT NULL,
|
||||
used integer NOT NULL
|
||||
);
|
||||
|
||||
--
|
||||
-- TOC entry 220 (class 1259 OID 1893965)
|
||||
-- Name: storage_profile_types; Type: TABLE; Schema: ngcloud_ru; Owner: vc_billing
|
||||
--
|
||||
DROP TABLE IF EXISTS ngcloud_ru.storage_profile_types;
|
||||
CREATE TABLE ngcloud_ru.storage_profile_types (
|
||||
id integer NOT NULL,
|
||||
name character varying(255) NOT NULL
|
||||
);
|
||||
|
||||
--
|
||||
-- TOC entry 218 (class 1259 OID 1893959)
|
||||
-- Name: tenant; Type: TABLE; Schema: ngcloud_ru; Owner: vc_billing
|
||||
--
|
||||
DROP TABLE IF EXISTS ngcloud_ru.tenant;
|
||||
CREATE TABLE ngcloud_ru.tenant (
|
||||
id uuid NOT NULL,
|
||||
name character varying(255) NOT NULL,
|
||||
wzcode bigint
|
||||
);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 221 (class 1259 OID 1893973)
|
||||
-- Name: vdc; Type: TABLE; Schema: ngcloud_ru; Owner: vc_billing
|
||||
--
|
||||
DROP TABLE IF EXISTS ngcloud_ru.vdc;
|
||||
CREATE TABLE ngcloud_ru.vdc (
|
||||
id uuid NOT NULL,
|
||||
tenant_id uuid NOT NULL,
|
||||
name character varying(255) NOT NULL
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3255 (class 2606 OID 1893987)
|
||||
-- Name: capacity_resource capacity_resource_pkey; Type: CONSTRAINT; Schema: ngcloud_ru; Owner: vc_billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY ngcloud_ru.capacity_resource
|
||||
ADD CONSTRAINT capacity_resource_pkey PRIMARY KEY (vdc_id, "timestamp");
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3257 (class 2606 OID 1893997)
|
||||
-- Name: capacity_storage capacity_storage_pkey; Type: CONSTRAINT; Schema: ngcloud_ru; Owner: vc_billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY ngcloud_ru.capacity_storage
|
||||
ADD CONSTRAINT capacity_storage_pkey PRIMARY KEY (vdc_id, id, "timestamp");
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3249 (class 2606 OID 1893972)
|
||||
-- Name: storage_profile_types storage_profile_types_name_key; Type: CONSTRAINT; Schema: ngcloud_ru; Owner: vc_billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY ngcloud_ru.storage_profile_types
|
||||
ADD CONSTRAINT storage_profile_types_name_key UNIQUE (name);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3251 (class 2606 OID 1893970)
|
||||
-- Name: storage_profile_types storage_profile_types_pkey; Type: CONSTRAINT; Schema: ngcloud_ru; Owner: vc_billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY ngcloud_ru.storage_profile_types
|
||||
ADD CONSTRAINT storage_profile_types_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3247 (class 2606 OID 1893963)
|
||||
-- Name: tenant tenant_pkey; Type: CONSTRAINT; Schema: ngcloud_ru; Owner: vc_billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY ngcloud_ru.tenant
|
||||
ADD CONSTRAINT tenant_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3253 (class 2606 OID 1893977)
|
||||
-- Name: vdc vdc_pkey; Type: CONSTRAINT; Schema: ngcloud_ru; Owner: vc_billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY ngcloud_ru.vdc
|
||||
ADD CONSTRAINT vdc_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3259 (class 2606 OID 1893988)
|
||||
-- Name: capacity_resource capacity_resource_vdc_id_fkey; Type: FK CONSTRAINT; Schema: ngcloud_ru; Owner: vc_billing
|
||||
--
|
||||
|
||||
--ALTER TABLE ONLY ngcloud_ru.capacity_resource
|
||||
-- ADD CONSTRAINT capacity_resource_vdc_id_fkey FOREIGN KEY (vdc_id) REFERENCES ngcloud_ru.vdc(id) ON DELETE CASCADE;
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3260 (class 2606 OID 1894003)
|
||||
-- Name: capacity_storage capacity_storage_storage_profile_types_id_fkey; Type: FK CONSTRAINT; Schema: ngcloud_ru; Owner: vc_billing
|
||||
--
|
||||
|
||||
--ALTER TABLE ONLY ngcloud_ru.capacity_storage
|
||||
-- ADD CONSTRAINT capacity_storage_storage_profile_types_id_fkey FOREIGN KEY (storage_profile_types_id) REFERENCES ngcloud_ru.storage_profile_types(id);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3261 (class 2606 OID 1893998)
|
||||
-- Name: capacity_storage capacity_storage_vdc_id_fkey; Type: FK CONSTRAINT; Schema: ngcloud_ru; Owner: vc_billing
|
||||
--
|
||||
|
||||
--ALTER TABLE ONLY ngcloud_ru.capacity_storage
|
||||
-- ADD CONSTRAINT capacity_storage_vdc_id_fkey FOREIGN KEY (vdc_id) REFERENCES ngcloud_ru.vdc(id) ON DELETE CASCADE;
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3258 (class 2606 OID 1893978)
|
||||
-- Name: vdc vdc_tenant_id_fkey; Type: FK CONSTRAINT; Schema: ngcloud_ru; Owner: vc_billing
|
||||
--
|
||||
|
||||
--ALTER TABLE ONLY ngcloud_ru.vdc
|
||||
-- ADD CONSTRAINT vdc_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES ngcloud_ru.tenant(id);
|
||||
|
||||
|
||||
-- Completed on 2025-09-15 09:53:18
|
||||
|
||||
--
|
||||
-- PostgreSQL database dump complete
|
||||
--
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
set role postgres;
|
||||
--
|
||||
CREATE SCHEMA IF NOT EXISTS gpu;
|
||||
--ALTER SCHEMA gpu OWNER TO dbo;
|
||||
GRANT USAGE ON SCHEMA gpu TO nifi;
|
||||
|
||||
GRANT ALL ON ALL TABLES IN SCHEMA gpu TO nifi;
|
||||
GRANT ALL ON ALL SEQUENCES IN SCHEMA gpu TO nifi;
|
||||
|
||||
--ALTER DEFAULT PRIVILEGES IN SCHEMA gpu GRANT ALL ON TABLES TO nifi;
|
||||
--ALTER DEFAULT PRIVILEGES IN SCHEMA gpu GRANT ALL ON SEQUENCES TO nifi;
|
||||
|
||||
ALTER DEFAULT PRIVILEGES FOR ROLE dbo, postgres IN SCHEMA gpu GRANT ALL ON TABLES TO nifi;
|
||||
ALTER DEFAULT PRIVILEGES FOR ROLE dbo, postgres IN SCHEMA gpu GRANT ALL ON SEQUENCES TO nifi;
|
||||
|
||||
--SET ROLE dbo;
|
||||
|
||||
DROP TABLE IF EXISTS gpu.vm;
|
||||
CREATE TABLE gpu.vm (
|
||||
objUUID uuid NOT NULL PRIMARY KEY
|
||||
,groupUUID uuid NULL
|
||||
,objId varchar(255) NULL
|
||||
,ClientID varchar(255) NULL -- WZ
|
||||
,VCD varchar(255) NULL
|
||||
,Org varchar(255) NULL
|
||||
,OrgVDC varchar(255) NULL
|
||||
,vCenter varchar(255) NULL
|
||||
,GPU varchar(255) NULL
|
||||
,dt_load timestamptz NOT NULL
|
||||
);
|
||||
ALTER TABLE gpu.vm OWNER TO dbo;
|
||||
/*
|
||||
{
|
||||
"type": "record",
|
||||
"name": "gpu.vm",
|
||||
"namespace": "org.apache.nifi",
|
||||
"fields": [
|
||||
{"name":"objUUID", "type":{"type": "string", "logicalType": "uuid"}},
|
||||
{"name":"groupUUID", "type":[{"type": "string", "logicalType": "uuid"} ,"null"]},
|
||||
{"name":"objId", "type":["string" ,"null"]},
|
||||
{"name":"ClientID", "type":["int" ,"null"]},
|
||||
{"name":"VCD", "type":["string" ,"null"]},
|
||||
{"name":"Org", "type":["string" ,"null"]},
|
||||
{"name":"OrgVDC", "type":["string" ,"null"]},
|
||||
{"name":"vCenter", "type":["string" ,"null"]},
|
||||
{"name":"GPU", "type":["string" ,"null"]},
|
||||
{"name":"VCD", "type":["string" ,"null"]},
|
||||
{"name":"dt_load","type":{"type": "long", "logicalType": "local-timestamp-millis"}}
|
||||
]
|
||||
}
|
||||
*/
|
||||
|
||||
DROP TABLE IF EXISTS gpu.vm_power_state;
|
||||
CREATE TABLE gpu.vm_power_state (
|
||||
objId varchar(255) NULL
|
||||
,ts timestamptz NOT NULL -- без таймштампа загрузки
|
||||
,value int NULL
|
||||
,CONSTRAINT PK_vm_power_state PRIMARY KEY(objId, ts)
|
||||
);
|
||||
ALTER TABLE gpu.vm_power_state OWNER TO dbo;
|
||||
/*
|
||||
{
|
||||
"type": "record",
|
||||
"name": "gpu.vm_power_state,
|
||||
"namespace": "org.apache.nifi",
|
||||
"fields": [
|
||||
{"name":"objId", "type":["string" ,"null"]},
|
||||
{"name":"ts","type":{"type": "long", "logicalType": "local-timestamp-millis"}},
|
||||
{"name":"value", "type":["int" ,"null"]}
|
||||
]
|
||||
}
|
||||
*/
|
||||
@@ -1,828 +0,0 @@
|
||||
--
|
||||
-- PostgreSQL database dump
|
||||
--
|
||||
|
||||
-- Dumped from database version 16.0
|
||||
-- Dumped by pg_dump version 16.4
|
||||
|
||||
-- Started on 2025-09-15 09:50:58
|
||||
|
||||
SET statement_timeout = 0;
|
||||
SET lock_timeout = 0;
|
||||
SET idle_in_transaction_session_timeout = 0;
|
||||
SET client_encoding = 'UTF8';
|
||||
SET standard_conforming_strings = on;
|
||||
SELECT pg_catalog.set_config('search_path', '', false);
|
||||
SET check_function_bodies = false;
|
||||
SET xmloption = content;
|
||||
SET client_min_messages = warning;
|
||||
SET row_security = off;
|
||||
|
||||
SET default_tablespace = '';
|
||||
|
||||
SET default_table_access_method = heap;
|
||||
|
||||
--
|
||||
-- TOC entry 230 (class 1259 OID 2160668)
|
||||
-- Name: bucket_info; Type: TABLE; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
CREATE TABLE s3billing.bucket_info (
|
||||
id integer NOT NULL,
|
||||
bucket_id character varying(255) NOT NULL,
|
||||
name character varying(255) NOT NULL,
|
||||
marker character varying(255) NOT NULL,
|
||||
placement_id bigint DEFAULT 0 NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE s3billing.bucket_info OWNER TO s3billing;
|
||||
|
||||
--
|
||||
-- TOC entry 229 (class 1259 OID 2160667)
|
||||
-- Name: bucket_info_id_seq; Type: SEQUENCE; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
CREATE SEQUENCE s3billing.bucket_info_id_seq
|
||||
AS integer
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER SEQUENCE s3billing.bucket_info_id_seq OWNER TO s3billing;
|
||||
|
||||
--
|
||||
-- TOC entry 3448 (class 0 OID 0)
|
||||
-- Dependencies: 229
|
||||
-- Name: bucket_info_id_seq; Type: SEQUENCE OWNED BY; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER SEQUENCE s3billing.bucket_info_id_seq OWNED BY s3billing.bucket_info.id;
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 226 (class 1259 OID 2160642)
|
||||
-- Name: bucket_stat; Type: TABLE; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
CREATE TABLE s3billing.bucket_stat (
|
||||
bucket_id bigint,
|
||||
num_shards bigint NOT NULL,
|
||||
tenant character varying(255) DEFAULT NULL::character varying,
|
||||
zone_group bigint NOT NULL,
|
||||
placement_id bigint NOT NULL,
|
||||
explicit_placement_data_pool character varying(255) DEFAULT NULL::character varying,
|
||||
explicit_placement_data_extra_pool character varying(255) DEFAULT NULL::character varying,
|
||||
explicit_placement_index_pool character varying(255) DEFAULT NULL::character varying,
|
||||
index_type_id bigint NOT NULL,
|
||||
versioned boolean DEFAULT false,
|
||||
versioning_enabled boolean DEFAULT false,
|
||||
object_lock_enabled boolean DEFAULT false,
|
||||
mfa_enabled boolean DEFAULT false,
|
||||
owner character varying(255) NOT NULL,
|
||||
bucket_quota_enabled boolean DEFAULT false,
|
||||
bucket_quota_check_on_raw boolean DEFAULT false,
|
||||
bucket_quota_max_size bigint NOT NULL,
|
||||
bucket_quota_max_size_kb bigint NOT NULL,
|
||||
bucket_quota_max_objects bigint NOT NULL,
|
||||
usage_rgw_main_size bigint NOT NULL,
|
||||
usage_rgw_main_size_actual bigint NOT NULL,
|
||||
usage_rgw_main_size_utilized bigint NOT NULL,
|
||||
usage_rgw_main_size_kb bigint NOT NULL,
|
||||
usage_rgw_main_size_kb_actual bigint NOT NULL,
|
||||
usage_rgw_main_size_kb_utilized bigint NOT NULL,
|
||||
usage_rgw_main_num_objects bigint NOT NULL,
|
||||
usage_rgw_multimeta_size bigint,
|
||||
usage_rgw_multimeta_size_actual bigint,
|
||||
usage_rgw_multimeta_size_utilized bigint,
|
||||
usage_rgw_multimeta_size_kb bigint,
|
||||
usage_rgw_multimeta_size_kb_actual bigint,
|
||||
usage_rgw_multimeta_size_kb_utilized bigint,
|
||||
usage_rgw_multimeta_num_objects bigint,
|
||||
creation_time timestamp without time zone NOT NULL,
|
||||
mtime timestamp without time zone NOT NULL,
|
||||
timestamp_addition timestamp without time zone DEFAULT date_trunc('hour'::text, CURRENT_TIMESTAMP)
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE s3billing.bucket_stat OWNER TO s3billing;
|
||||
|
||||
--
|
||||
-- TOC entry 245 (class 1259 OID 2160763)
|
||||
-- Name: category; Type: TABLE; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
CREATE TABLE s3billing.category (
|
||||
id integer NOT NULL,
|
||||
name character varying(255) NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE s3billing.category OWNER TO s3billing;
|
||||
|
||||
--
|
||||
-- TOC entry 244 (class 1259 OID 2160762)
|
||||
-- Name: category_id_seq; Type: SEQUENCE; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
CREATE SEQUENCE s3billing.category_id_seq
|
||||
AS integer
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER SEQUENCE s3billing.category_id_seq OWNER TO s3billing;
|
||||
|
||||
--
|
||||
-- TOC entry 3451 (class 0 OID 0)
|
||||
-- Dependencies: 244
|
||||
-- Name: category_id_seq; Type: SEQUENCE OWNED BY; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER SEQUENCE s3billing.category_id_seq OWNED BY s3billing.category.id;
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 225 (class 1259 OID 2160635)
|
||||
-- Name: goose_db_version; Type: TABLE; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
CREATE TABLE s3billing.goose_db_version (
|
||||
id integer NOT NULL,
|
||||
version_id bigint NOT NULL,
|
||||
is_applied boolean NOT NULL,
|
||||
tstamp timestamp without time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE s3billing.goose_db_version OWNER TO s3billing;
|
||||
|
||||
--
|
||||
-- TOC entry 224 (class 1259 OID 2160634)
|
||||
-- Name: goose_db_version_id_seq; Type: SEQUENCE; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER TABLE s3billing.goose_db_version ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
||||
SEQUENCE NAME s3billing.goose_db_version_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1
|
||||
);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 234 (class 1259 OID 2160684)
|
||||
-- Name: index_type; Type: TABLE; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
CREATE TABLE s3billing.index_type (
|
||||
id integer NOT NULL,
|
||||
name character varying(255) NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE s3billing.index_type OWNER TO s3billing;
|
||||
|
||||
--
|
||||
-- TOC entry 233 (class 1259 OID 2160683)
|
||||
-- Name: index_type_id_seq; Type: SEQUENCE; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
CREATE SEQUENCE s3billing.index_type_id_seq
|
||||
AS integer
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER SEQUENCE s3billing.index_type_id_seq OWNER TO s3billing;
|
||||
|
||||
--
|
||||
-- TOC entry 3454 (class 0 OID 0)
|
||||
-- Dependencies: 233
|
||||
-- Name: index_type_id_seq; Type: SEQUENCE OWNED BY; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER SEQUENCE s3billing.index_type_id_seq OWNED BY s3billing.index_type.id;
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 232 (class 1259 OID 2160677)
|
||||
-- Name: placement; Type: TABLE; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
CREATE TABLE s3billing.placement (
|
||||
id integer NOT NULL,
|
||||
name character varying(255) NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE s3billing.placement OWNER TO s3billing;
|
||||
|
||||
--
|
||||
-- TOC entry 231 (class 1259 OID 2160676)
|
||||
-- Name: placement_id_seq; Type: SEQUENCE; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
CREATE SEQUENCE s3billing.placement_id_seq
|
||||
AS integer
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER SEQUENCE s3billing.placement_id_seq OWNER TO s3billing;
|
||||
|
||||
--
|
||||
-- TOC entry 3456 (class 0 OID 0)
|
||||
-- Dependencies: 231
|
||||
-- Name: placement_id_seq; Type: SEQUENCE OWNED BY; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER SEQUENCE s3billing.placement_id_seq OWNED BY s3billing.placement.id;
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 239 (class 1259 OID 2160705)
|
||||
-- Name: usage_bucket_by_user; Type: TABLE; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
CREATE TABLE s3billing.usage_bucket_by_user (
|
||||
bucketid bigint,
|
||||
"time" timestamp without time zone NOT NULL,
|
||||
bytes_sent bigint NOT NULL,
|
||||
bytes_received bigint NOT NULL,
|
||||
ops bigint NOT NULL,
|
||||
successful_ops bigint NOT NULL,
|
||||
date_start timestamp without time zone NOT NULL,
|
||||
date_end timestamp without time zone NOT NULL,
|
||||
user_id bigint NOT NULL,
|
||||
category_id bigint NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE s3billing.usage_bucket_by_user OWNER TO s3billing;
|
||||
|
||||
--
|
||||
-- TOC entry 238 (class 1259 OID 2160698)
|
||||
-- Name: usage_stat_summary; Type: TABLE; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
CREATE TABLE s3billing.usage_stat_summary (
|
||||
id integer NOT NULL,
|
||||
userid character varying(255) NOT NULL,
|
||||
category character varying(255) NOT NULL,
|
||||
bytes_sent bigint NOT NULL,
|
||||
bytes_received bigint NOT NULL,
|
||||
ops bigint NOT NULL,
|
||||
successful_ops bigint NOT NULL,
|
||||
date_start timestamp without time zone NOT NULL,
|
||||
date_end timestamp without time zone NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE s3billing.usage_stat_summary OWNER TO s3billing;
|
||||
|
||||
--
|
||||
-- TOC entry 237 (class 1259 OID 2160697)
|
||||
-- Name: usage_stat_summary_id_seq; Type: SEQUENCE; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
CREATE SEQUENCE s3billing.usage_stat_summary_id_seq
|
||||
AS integer
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER SEQUENCE s3billing.usage_stat_summary_id_seq OWNER TO s3billing;
|
||||
|
||||
--
|
||||
-- TOC entry 3459 (class 0 OID 0)
|
||||
-- Dependencies: 237
|
||||
-- Name: usage_stat_summary_id_seq; Type: SEQUENCE OWNED BY; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER SEQUENCE s3billing.usage_stat_summary_id_seq OWNED BY s3billing.usage_stat_summary.id;
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 236 (class 1259 OID 2160691)
|
||||
-- Name: usage_stat_total; Type: TABLE; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
CREATE TABLE s3billing.usage_stat_total (
|
||||
id integer NOT NULL,
|
||||
userid character varying(255) NOT NULL,
|
||||
bytes_sent bigint NOT NULL,
|
||||
bytes_received bigint NOT NULL,
|
||||
ops bigint NOT NULL,
|
||||
successful_ops bigint NOT NULL,
|
||||
date_start timestamp without time zone NOT NULL,
|
||||
date_end timestamp without time zone NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE s3billing.usage_stat_total OWNER TO s3billing;
|
||||
|
||||
--
|
||||
-- TOC entry 235 (class 1259 OID 2160690)
|
||||
-- Name: usage_stat_total_id_seq; Type: SEQUENCE; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
CREATE SEQUENCE s3billing.usage_stat_total_id_seq
|
||||
AS integer
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER SEQUENCE s3billing.usage_stat_total_id_seq OWNER TO s3billing;
|
||||
|
||||
--
|
||||
-- TOC entry 3461 (class 0 OID 0)
|
||||
-- Dependencies: 235
|
||||
-- Name: usage_stat_total_id_seq; Type: SEQUENCE OWNED BY; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER SEQUENCE s3billing.usage_stat_total_id_seq OWNED BY s3billing.usage_stat_total.id;
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 243 (class 1259 OID 2160751)
|
||||
-- Name: user_info; Type: TABLE; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
CREATE TABLE s3billing.user_info (
|
||||
id integer NOT NULL,
|
||||
name character varying(255) NOT NULL,
|
||||
owner character varying(255) NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE s3billing.user_info OWNER TO s3billing;
|
||||
|
||||
--
|
||||
-- TOC entry 242 (class 1259 OID 2160750)
|
||||
-- Name: user_info_id_seq; Type: SEQUENCE; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
CREATE SEQUENCE s3billing.user_info_id_seq
|
||||
AS integer
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER SEQUENCE s3billing.user_info_id_seq OWNER TO s3billing;
|
||||
|
||||
--
|
||||
-- TOC entry 3463 (class 0 OID 0)
|
||||
-- Dependencies: 242
|
||||
-- Name: user_info_id_seq; Type: SEQUENCE OWNED BY; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER SEQUENCE s3billing.user_info_id_seq OWNED BY s3billing.user_info.id;
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 241 (class 1259 OID 2160712)
|
||||
-- Name: user_quota_bucket; Type: TABLE; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
CREATE TABLE s3billing.user_quota_bucket (
|
||||
id integer NOT NULL,
|
||||
userid character varying(255) NOT NULL,
|
||||
enabled boolean DEFAULT false,
|
||||
check_on_raw boolean DEFAULT false,
|
||||
max_size bigint NOT NULL,
|
||||
max_size_kb bigint NOT NULL,
|
||||
max_objects bigint NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE s3billing.user_quota_bucket OWNER TO s3billing;
|
||||
|
||||
--
|
||||
-- TOC entry 240 (class 1259 OID 2160711)
|
||||
-- Name: user_quota_bucket_id_seq; Type: SEQUENCE; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
CREATE SEQUENCE s3billing.user_quota_bucket_id_seq
|
||||
AS integer
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER SEQUENCE s3billing.user_quota_bucket_id_seq OWNER TO s3billing;
|
||||
|
||||
--
|
||||
-- TOC entry 3465 (class 0 OID 0)
|
||||
-- Dependencies: 240
|
||||
-- Name: user_quota_bucket_id_seq; Type: SEQUENCE OWNED BY; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER SEQUENCE s3billing.user_quota_bucket_id_seq OWNED BY s3billing.user_quota_bucket.id;
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 228 (class 1259 OID 2160661)
|
||||
-- Name: zone_group; Type: TABLE; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
CREATE TABLE s3billing.zone_group (
|
||||
id integer NOT NULL,
|
||||
name uuid NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE s3billing.zone_group OWNER TO s3billing;
|
||||
|
||||
--
|
||||
-- TOC entry 227 (class 1259 OID 2160660)
|
||||
-- Name: zone_group_id_seq; Type: SEQUENCE; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
CREATE SEQUENCE s3billing.zone_group_id_seq
|
||||
AS integer
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER SEQUENCE s3billing.zone_group_id_seq OWNER TO s3billing;
|
||||
|
||||
--
|
||||
-- TOC entry 3467 (class 0 OID 0)
|
||||
-- Dependencies: 227
|
||||
-- Name: zone_group_id_seq; Type: SEQUENCE OWNED BY; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER SEQUENCE s3billing.zone_group_id_seq OWNED BY s3billing.zone_group.id;
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3258 (class 2604 OID 2160671)
|
||||
-- Name: bucket_info id; Type: DEFAULT; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY s3billing.bucket_info ALTER COLUMN id SET DEFAULT nextval('s3billing.bucket_info_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3268 (class 2604 OID 2160766)
|
||||
-- Name: category id; Type: DEFAULT; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY s3billing.category ALTER COLUMN id SET DEFAULT nextval('s3billing.category_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3261 (class 2604 OID 2160687)
|
||||
-- Name: index_type id; Type: DEFAULT; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY s3billing.index_type ALTER COLUMN id SET DEFAULT nextval('s3billing.index_type_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3260 (class 2604 OID 2160680)
|
||||
-- Name: placement id; Type: DEFAULT; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY s3billing.placement ALTER COLUMN id SET DEFAULT nextval('s3billing.placement_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3263 (class 2604 OID 2160701)
|
||||
-- Name: usage_stat_summary id; Type: DEFAULT; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY s3billing.usage_stat_summary ALTER COLUMN id SET DEFAULT nextval('s3billing.usage_stat_summary_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3262 (class 2604 OID 2160694)
|
||||
-- Name: usage_stat_total id; Type: DEFAULT; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY s3billing.usage_stat_total ALTER COLUMN id SET DEFAULT nextval('s3billing.usage_stat_total_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3267 (class 2604 OID 2160754)
|
||||
-- Name: user_info id; Type: DEFAULT; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY s3billing.user_info ALTER COLUMN id SET DEFAULT nextval('s3billing.user_info_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3264 (class 2604 OID 2160715)
|
||||
-- Name: user_quota_bucket id; Type: DEFAULT; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY s3billing.user_quota_bucket ALTER COLUMN id SET DEFAULT nextval('s3billing.user_quota_bucket_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3257 (class 2604 OID 2160664)
|
||||
-- Name: zone_group id; Type: DEFAULT; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY s3billing.zone_group ALTER COLUMN id SET DEFAULT nextval('s3billing.zone_group_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3278 (class 2606 OID 2160675)
|
||||
-- Name: bucket_info bucket_info_pkey; Type: CONSTRAINT; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY s3billing.bucket_info
|
||||
ADD CONSTRAINT bucket_info_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3273 (class 2606 OID 2476484)
|
||||
-- Name: bucket_stat bucket_stat_unique; Type: CONSTRAINT; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY s3billing.bucket_stat
|
||||
ADD CONSTRAINT bucket_stat_unique UNIQUE (bucket_id, placement_id, timestamp_addition);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3298 (class 2606 OID 2160768)
|
||||
-- Name: category category_pkey; Type: CONSTRAINT; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY s3billing.category
|
||||
ADD CONSTRAINT category_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3270 (class 2606 OID 2160640)
|
||||
-- Name: goose_db_version goose_db_version_pkey; Type: CONSTRAINT; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY s3billing.goose_db_version
|
||||
ADD CONSTRAINT goose_db_version_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3284 (class 2606 OID 2160689)
|
||||
-- Name: index_type index_type_pkey; Type: CONSTRAINT; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY s3billing.index_type
|
||||
ADD CONSTRAINT index_type_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3282 (class 2606 OID 2160682)
|
||||
-- Name: placement placement_pkey; Type: CONSTRAINT; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY s3billing.placement
|
||||
ADD CONSTRAINT placement_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3290 (class 2606 OID 2476705)
|
||||
-- Name: usage_bucket_by_user usage_bucket_by_user_unique; Type: CONSTRAINT; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY s3billing.usage_bucket_by_user
|
||||
ADD CONSTRAINT usage_bucket_by_user_unique UNIQUE (bucketid, date_start, date_end, user_id, category_id);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3288 (class 2606 OID 2160703)
|
||||
-- Name: usage_stat_summary usage_stat_summary_pkey; Type: CONSTRAINT; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY s3billing.usage_stat_summary
|
||||
ADD CONSTRAINT usage_stat_summary_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3286 (class 2606 OID 2160696)
|
||||
-- Name: usage_stat_total usage_stat_total_pkey; Type: CONSTRAINT; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY s3billing.usage_stat_total
|
||||
ADD CONSTRAINT usage_stat_total_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3295 (class 2606 OID 2160758)
|
||||
-- Name: user_info user_info_pkey; Type: CONSTRAINT; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY s3billing.user_info
|
||||
ADD CONSTRAINT user_info_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3292 (class 2606 OID 2160719)
|
||||
-- Name: user_quota_bucket user_quota_bucket_pkey; Type: CONSTRAINT; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY s3billing.user_quota_bucket
|
||||
ADD CONSTRAINT user_quota_bucket_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3275 (class 2606 OID 2160666)
|
||||
-- Name: zone_group zone_group_pkey; Type: CONSTRAINT; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY s3billing.zone_group
|
||||
ADD CONSTRAINT zone_group_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3276 (class 1259 OID 2474077)
|
||||
-- Name: bucket_info_name_idx; Type: INDEX; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
CREATE INDEX bucket_info_name_idx ON s3billing.bucket_info USING btree (name);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3279 (class 1259 OID 2474078)
|
||||
-- Name: bucket_info_placement_id_idx; Type: INDEX; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
CREATE INDEX bucket_info_placement_id_idx ON s3billing.bucket_info USING btree (placement_id);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3271 (class 1259 OID 2474094)
|
||||
-- Name: bucket_stat_owner_idx; Type: INDEX; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
CREATE INDEX bucket_stat_owner_idx ON s3billing.bucket_stat USING btree (owner);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3296 (class 1259 OID 2474445)
|
||||
-- Name: category_name_idx; Type: INDEX; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
CREATE UNIQUE INDEX category_name_idx ON s3billing.category USING btree (name);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3280 (class 1259 OID 2474468)
|
||||
-- Name: placement_name_idx; Type: INDEX; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
CREATE UNIQUE INDEX placement_name_idx ON s3billing.placement USING btree (name);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3293 (class 1259 OID 2474438)
|
||||
-- Name: user_info_name_idx; Type: INDEX; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
CREATE INDEX user_info_name_idx ON s3billing.user_info USING btree (name);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3447 (class 0 OID 0)
|
||||
-- Dependencies: 230
|
||||
-- Name: TABLE bucket_info; Type: ACL; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
GRANT SELECT ON TABLE s3billing.bucket_info TO grafanabilling;
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3449 (class 0 OID 0)
|
||||
-- Dependencies: 226
|
||||
-- Name: TABLE bucket_stat; Type: ACL; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
GRANT SELECT ON TABLE s3billing.bucket_stat TO grafanabilling;
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3450 (class 0 OID 0)
|
||||
-- Dependencies: 245
|
||||
-- Name: TABLE category; Type: ACL; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
GRANT SELECT ON TABLE s3billing.category TO grafanabilling;
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3452 (class 0 OID 0)
|
||||
-- Dependencies: 225
|
||||
-- Name: TABLE goose_db_version; Type: ACL; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
GRANT SELECT ON TABLE s3billing.goose_db_version TO grafanabilling;
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3453 (class 0 OID 0)
|
||||
-- Dependencies: 234
|
||||
-- Name: TABLE index_type; Type: ACL; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
GRANT SELECT ON TABLE s3billing.index_type TO grafanabilling;
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3455 (class 0 OID 0)
|
||||
-- Dependencies: 232
|
||||
-- Name: TABLE placement; Type: ACL; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
GRANT SELECT ON TABLE s3billing.placement TO grafanabilling;
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3457 (class 0 OID 0)
|
||||
-- Dependencies: 239
|
||||
-- Name: TABLE usage_bucket_by_user; Type: ACL; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
GRANT SELECT ON TABLE s3billing.usage_bucket_by_user TO grafanabilling;
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3458 (class 0 OID 0)
|
||||
-- Dependencies: 238
|
||||
-- Name: TABLE usage_stat_summary; Type: ACL; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
GRANT SELECT ON TABLE s3billing.usage_stat_summary TO grafanabilling;
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3460 (class 0 OID 0)
|
||||
-- Dependencies: 236
|
||||
-- Name: TABLE usage_stat_total; Type: ACL; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
GRANT SELECT ON TABLE s3billing.usage_stat_total TO grafanabilling;
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3462 (class 0 OID 0)
|
||||
-- Dependencies: 243
|
||||
-- Name: TABLE user_info; Type: ACL; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
GRANT SELECT ON TABLE s3billing.user_info TO grafanabilling;
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3464 (class 0 OID 0)
|
||||
-- Dependencies: 241
|
||||
-- Name: TABLE user_quota_bucket; Type: ACL; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
GRANT SELECT ON TABLE s3billing.user_quota_bucket TO grafanabilling;
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3466 (class 0 OID 0)
|
||||
-- Dependencies: 228
|
||||
-- Name: TABLE zone_group; Type: ACL; Schema: s3billing; Owner: s3billing
|
||||
--
|
||||
|
||||
GRANT SELECT ON TABLE s3billing.zone_group TO grafanabilling;
|
||||
|
||||
|
||||
-- Completed on 2025-09-15 09:50:59
|
||||
|
||||
--
|
||||
-- PostgreSQL database dump complete
|
||||
--
|
||||
|
||||
@@ -1,278 +0,0 @@
|
||||
--
|
||||
-- PostgreSQL database dump
|
||||
--
|
||||
|
||||
-- Dumped from database version 16.0
|
||||
-- Dumped by pg_dump version 16.4
|
||||
|
||||
-- Started on 2025-09-15 09:53:16
|
||||
|
||||
SET statement_timeout = 0;
|
||||
SET lock_timeout = 0;
|
||||
SET idle_in_transaction_session_timeout = 0;
|
||||
SET client_encoding = 'UTF8';
|
||||
SET standard_conforming_strings = on;
|
||||
SELECT pg_catalog.set_config('search_path', '', false);
|
||||
SET check_function_bodies = false;
|
||||
SET xmloption = content;
|
||||
SET client_min_messages = warning;
|
||||
SET row_security = off;
|
||||
|
||||
SET default_tablespace = '';
|
||||
|
||||
SET default_table_access_method = heap;
|
||||
|
||||
--
|
||||
-- TOC entry 222 (class 1259 OID 1893983)
|
||||
-- Name: capacity_resource; Type: TABLE; Schema: ngcloud_ru; Owner: vc_billing
|
||||
--
|
||||
|
||||
CREATE TABLE ngcloud_ru.capacity_resource (
|
||||
vdc_id uuid NOT NULL,
|
||||
"timestamp" timestamp with time zone NOT NULL,
|
||||
cpu_allocated integer NOT NULL,
|
||||
cpu_limit integer NOT NULL,
|
||||
cpu_reserved integer NOT NULL,
|
||||
cpu_used integer NOT NULL,
|
||||
cpu_speed integer NOT NULL,
|
||||
mem_allocated integer NOT NULL,
|
||||
mem_limit integer NOT NULL,
|
||||
mem_reserved integer NOT NULL,
|
||||
mem_used integer NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE ngcloud_ru.capacity_resource OWNER TO vc_billing;
|
||||
|
||||
--
|
||||
-- TOC entry 223 (class 1259 OID 1893993)
|
||||
-- Name: capacity_storage; Type: TABLE; Schema: ngcloud_ru; Owner: vc_billing
|
||||
--
|
||||
|
||||
CREATE TABLE ngcloud_ru.capacity_storage (
|
||||
vdc_id uuid NOT NULL,
|
||||
storage_profile_types_id integer NOT NULL,
|
||||
"timestamp" timestamp with time zone NOT NULL,
|
||||
id uuid NOT NULL,
|
||||
"limit" integer NOT NULL,
|
||||
used integer NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE ngcloud_ru.capacity_storage OWNER TO vc_billing;
|
||||
|
||||
--
|
||||
-- TOC entry 220 (class 1259 OID 1893965)
|
||||
-- Name: storage_profile_types; Type: TABLE; Schema: ngcloud_ru; Owner: vc_billing
|
||||
--
|
||||
|
||||
CREATE TABLE ngcloud_ru.storage_profile_types (
|
||||
id integer NOT NULL,
|
||||
name character varying(255) NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE ngcloud_ru.storage_profile_types OWNER TO vc_billing;
|
||||
|
||||
--
|
||||
-- TOC entry 219 (class 1259 OID 1893964)
|
||||
-- Name: storage_profile_types_id_seq; Type: SEQUENCE; Schema: ngcloud_ru; Owner: vc_billing
|
||||
--
|
||||
|
||||
CREATE SEQUENCE ngcloud_ru.storage_profile_types_id_seq
|
||||
AS integer
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER SEQUENCE ngcloud_ru.storage_profile_types_id_seq OWNER TO vc_billing;
|
||||
|
||||
--
|
||||
-- TOC entry 3413 (class 0 OID 0)
|
||||
-- Dependencies: 219
|
||||
-- Name: storage_profile_types_id_seq; Type: SEQUENCE OWNED BY; Schema: ngcloud_ru; Owner: vc_billing
|
||||
--
|
||||
|
||||
ALTER SEQUENCE ngcloud_ru.storage_profile_types_id_seq OWNED BY ngcloud_ru.storage_profile_types.id;
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 218 (class 1259 OID 1893959)
|
||||
-- Name: tenant; Type: TABLE; Schema: ngcloud_ru; Owner: vc_billing
|
||||
--
|
||||
|
||||
CREATE TABLE ngcloud_ru.tenant (
|
||||
id uuid NOT NULL,
|
||||
name character varying(255) NOT NULL,
|
||||
wzcode bigint
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE ngcloud_ru.tenant OWNER TO vc_billing;
|
||||
|
||||
--
|
||||
-- TOC entry 221 (class 1259 OID 1893973)
|
||||
-- Name: vdc; Type: TABLE; Schema: ngcloud_ru; Owner: vc_billing
|
||||
--
|
||||
|
||||
CREATE TABLE ngcloud_ru.vdc (
|
||||
id uuid NOT NULL,
|
||||
tenant_id uuid NOT NULL,
|
||||
name character varying(255) NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE ngcloud_ru.vdc OWNER TO vc_billing;
|
||||
|
||||
--
|
||||
-- TOC entry 3245 (class 2604 OID 1893968)
|
||||
-- Name: storage_profile_types id; Type: DEFAULT; Schema: ngcloud_ru; Owner: vc_billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY ngcloud_ru.storage_profile_types ALTER COLUMN id SET DEFAULT nextval('ngcloud_ru.storage_profile_types_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3255 (class 2606 OID 1893987)
|
||||
-- Name: capacity_resource capacity_resource_pkey; Type: CONSTRAINT; Schema: ngcloud_ru; Owner: vc_billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY ngcloud_ru.capacity_resource
|
||||
ADD CONSTRAINT capacity_resource_pkey PRIMARY KEY (vdc_id, "timestamp");
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3257 (class 2606 OID 1893997)
|
||||
-- Name: capacity_storage capacity_storage_pkey; Type: CONSTRAINT; Schema: ngcloud_ru; Owner: vc_billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY ngcloud_ru.capacity_storage
|
||||
ADD CONSTRAINT capacity_storage_pkey PRIMARY KEY (vdc_id, id, "timestamp");
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3249 (class 2606 OID 1893972)
|
||||
-- Name: storage_profile_types storage_profile_types_name_key; Type: CONSTRAINT; Schema: ngcloud_ru; Owner: vc_billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY ngcloud_ru.storage_profile_types
|
||||
ADD CONSTRAINT storage_profile_types_name_key UNIQUE (name);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3251 (class 2606 OID 1893970)
|
||||
-- Name: storage_profile_types storage_profile_types_pkey; Type: CONSTRAINT; Schema: ngcloud_ru; Owner: vc_billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY ngcloud_ru.storage_profile_types
|
||||
ADD CONSTRAINT storage_profile_types_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3247 (class 2606 OID 1893963)
|
||||
-- Name: tenant tenant_pkey; Type: CONSTRAINT; Schema: ngcloud_ru; Owner: vc_billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY ngcloud_ru.tenant
|
||||
ADD CONSTRAINT tenant_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3253 (class 2606 OID 1893977)
|
||||
-- Name: vdc vdc_pkey; Type: CONSTRAINT; Schema: ngcloud_ru; Owner: vc_billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY ngcloud_ru.vdc
|
||||
ADD CONSTRAINT vdc_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3259 (class 2606 OID 1893988)
|
||||
-- Name: capacity_resource capacity_resource_vdc_id_fkey; Type: FK CONSTRAINT; Schema: ngcloud_ru; Owner: vc_billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY ngcloud_ru.capacity_resource
|
||||
ADD CONSTRAINT capacity_resource_vdc_id_fkey FOREIGN KEY (vdc_id) REFERENCES ngcloud_ru.vdc(id) ON DELETE CASCADE;
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3260 (class 2606 OID 1894003)
|
||||
-- Name: capacity_storage capacity_storage_storage_profile_types_id_fkey; Type: FK CONSTRAINT; Schema: ngcloud_ru; Owner: vc_billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY ngcloud_ru.capacity_storage
|
||||
ADD CONSTRAINT capacity_storage_storage_profile_types_id_fkey FOREIGN KEY (storage_profile_types_id) REFERENCES ngcloud_ru.storage_profile_types(id);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3261 (class 2606 OID 1893998)
|
||||
-- Name: capacity_storage capacity_storage_vdc_id_fkey; Type: FK CONSTRAINT; Schema: ngcloud_ru; Owner: vc_billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY ngcloud_ru.capacity_storage
|
||||
ADD CONSTRAINT capacity_storage_vdc_id_fkey FOREIGN KEY (vdc_id) REFERENCES ngcloud_ru.vdc(id) ON DELETE CASCADE;
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3258 (class 2606 OID 1893978)
|
||||
-- Name: vdc vdc_tenant_id_fkey; Type: FK CONSTRAINT; Schema: ngcloud_ru; Owner: vc_billing
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY ngcloud_ru.vdc
|
||||
ADD CONSTRAINT vdc_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES ngcloud_ru.tenant(id);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3410 (class 0 OID 0)
|
||||
-- Dependencies: 222
|
||||
-- Name: TABLE capacity_resource; Type: ACL; Schema: ngcloud_ru; Owner: vc_billing
|
||||
--
|
||||
|
||||
GRANT SELECT ON TABLE ngcloud_ru.capacity_resource TO grafanabilling;
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3411 (class 0 OID 0)
|
||||
-- Dependencies: 223
|
||||
-- Name: TABLE capacity_storage; Type: ACL; Schema: ngcloud_ru; Owner: vc_billing
|
||||
--
|
||||
|
||||
GRANT SELECT ON TABLE ngcloud_ru.capacity_storage TO grafanabilling;
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3412 (class 0 OID 0)
|
||||
-- Dependencies: 220
|
||||
-- Name: TABLE storage_profile_types; Type: ACL; Schema: ngcloud_ru; Owner: vc_billing
|
||||
--
|
||||
|
||||
GRANT SELECT ON TABLE ngcloud_ru.storage_profile_types TO grafanabilling;
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3414 (class 0 OID 0)
|
||||
-- Dependencies: 218
|
||||
-- Name: TABLE tenant; Type: ACL; Schema: ngcloud_ru; Owner: vc_billing
|
||||
--
|
||||
|
||||
GRANT SELECT ON TABLE ngcloud_ru.tenant TO grafanabilling;
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3415 (class 0 OID 0)
|
||||
-- Dependencies: 221
|
||||
-- Name: TABLE vdc; Type: ACL; Schema: ngcloud_ru; Owner: vc_billing
|
||||
--
|
||||
|
||||
GRANT SELECT ON TABLE ngcloud_ru.vdc TO grafanabilling;
|
||||
|
||||
|
||||
-- Completed on 2025-09-15 09:53:18
|
||||
|
||||
--
|
||||
-- PostgreSQL database dump complete
|
||||
--
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
CREATE INDEX ON ngcloud_ru.capacity_resource (timestamp);
|
||||
CREATE INDEX ON ngcloud_ru.capacity_storage (timestamp);
|
||||
|
||||
CREATE INDEX ON s3billing.bucket_stat (timestamp_addition);
|
||||
CREATE INDEX ON s3billing.usage_bucket_by_user (time);
|
||||
|
||||
CREATE INDEX ON gpu.vm (dt_load);
|
||||
CREATE INDEX ON gpu.vm_power_state (ts);
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"ds_rpt_class": "org.postgresql.Driver",
|
||||
"ds_rpt_connectionString": "jdbc:postgresql://write.09d78921-77a7-401a-b2cb-5ca11be850a0.services.ngcloud.ru:5432/rpt",
|
||||
"ds_rpt_username": "appserver",
|
||||
"ds_rpt_password": "encrypted:********",
|
||||
"ds_rpt_connectionLimit": "5",
|
||||
"IDP_CLIENT_SECRET": "********",
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
REM set Windows env
|
||||
setx rpt_class org.postgresql.Driver /M
|
||||
setx rpt_connectionString jdbc:postgresql://write.09d78921-77a7-401a-b2cb-5ca11be850a0.services.ngcloud.ru:5432/rpt /M
|
||||
setx rpt_username appserver /M
|
||||
setx rpt_password encrypted:******** /M
|
||||
setx rpt_connectionLimit 5 /M
|
||||
|
||||
setx IDP_CLIENT_SECRET ******** /M
|
||||
|
||||
pause
|
||||
@@ -1,281 +0,0 @@
|
||||
<cfimport prefix="m" taglib="../lib"/>
|
||||
<cfimport prefix="d" taglib="../lib/data"/>
|
||||
|
||||
<cfparam name="ATTRIBUTES.dt_finish" type="date" default=#createDateTime(year(Now()),month(Now()),1,0,0,0)#/>
|
||||
<cfparam name="ATTRIBUTES.dt_start" type="date" default=#dateAdd('m',-1,ATTRIBUTES.dt_finish)#/>
|
||||
<cfparam name="ATTRIBUTES.output" type="string" default="report"/>
|
||||
<cfparam name="ATTRIBUTES.debug" type="boolean" default=false/>
|
||||
|
||||
<cfset hours=dateDiff('h',dt_start,dt_finish)/>
|
||||
<cfset tenant_wz_index=val(mid(request.auth.wz,3,5))/><!--- cut off WZ --->
|
||||
|
||||
|
||||
<!--- QoQ does not support join syntax, at least of Lucee 5.4--->
|
||||
|
||||
|
||||
<!--- Для начала выкатываем просто кусок каталога, не фильтруя по спеке (которой иногда нет) --->
|
||||
<cfquery name="qSpec">
|
||||
select * from (
|
||||
values
|
||||
('Объектное хранилище S3 CEPH','Дисковое пространство: Объем хранения','paas.s3.ceph.vol-m','ГБайт',1.80,0)
|
||||
,('Объектное хранилище S3 CEPH','Исходящий трафик','paas.s3.ceph.trf-m','ГБайт',1.50,0)
|
||||
,('Объектное хранилище S3 CEPH','Запросы GET, HEAD, OPTIONS','paas.s3.ceph.get-m','10тыс.шт.',0.35,0)
|
||||
,('Объектное хранилище S3 CEPH','Запросы PUT, POST, PATCH','paas.s3.ceph.put-m','10тыс.шт.',4.50,0)
|
||||
) as
|
||||
s (svc, component, code, unit, price, discount)
|
||||
order by code
|
||||
</cfquery>
|
||||
|
||||
|
||||
<!--- Нужно еще реализовать в модели данных политики бесплатного минимума --->
|
||||
<cfquery name="qComputing">
|
||||
select 'WZ'||to_char(tenant.wzcode,'FM00000') as wz, vdc.name as vdc_name,
|
||||
case
|
||||
when vdc.name like '%-v1cl1%' then 'iaas.ngc.i29'
|
||||
when vdc.name like '%-v1cl2%' then 'iaas.ngc.i31'
|
||||
when vdc.name like '%-v1cl4%' then 'iaas.ngc.a28'
|
||||
when vdc.name like '%-v1cl6%' then 'iaas.ngc.i28'
|
||||
else '['||vdc.name||']' end as code,
|
||||
sum(cpu_used/cpu_speed)/4. as core_h,
|
||||
sum(mem_used)/1024/4. as gb_h
|
||||
from ngcloud_ru.capacity_resource
|
||||
join ngcloud_ru.vdc on capacity_resource.vdc_id=vdc.id
|
||||
join ngcloud_ru.tenant on tenant.id=vdc.tenant_id
|
||||
where tenant.wzcode = <cfqueryparam cfsqltype="cf_sql_integer" value=#tenant_wz_index#/>
|
||||
AND capacity_resource.timestamp >= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#ATTRIBUTES.dt_start#/>
|
||||
AND capacity_resource.timestamp < <cfqueryparam cfsqltype="cf_sql_timestamp" value=#ATTRIBUTES.dt_finish#/>
|
||||
group by tenant.wzcode, vdc.name
|
||||
order by tenant.wzcode, vdc.name;
|
||||
</cfquery>
|
||||
|
||||
<cfif (ATTRIBUTES.debug)>
|
||||
<cfdump var=#qComputing#/>
|
||||
<cfflush/>
|
||||
</cfif>
|
||||
|
||||
<cfquery name="qComputingAge">
|
||||
select max(capacity_resource.timestamp) as dt_load from ngcloud_ru.capacity_resource
|
||||
</cfquery>
|
||||
|
||||
|
||||
<!--- переделать storage_profile_types с расшифровкой ssd, sata, можно прямо до каталожного кода --->
|
||||
<cfquery name="qStorage">
|
||||
select 'WZ'||to_char(tenant.wzcode,'FM00000') as wz, vdc.name as vdc_name,
|
||||
case
|
||||
WHEN vdc.name LIKE '%-v1cl1%' THEN 'iaas.ngc.i29'
|
||||
WHEN vdc.name LIKE '%-v1cl2%' THEN 'iaas.ngc.i31'
|
||||
WHEN vdc.name LIKE '%-v1cl4%' THEN 'iaas.ngc.a28'
|
||||
WHEN vdc.name LIKE '%-v1cl6%' THEN 'iaas.ngc.i28'
|
||||
ELSE '['||vdc.name||']' END
|
||||
|| CASE
|
||||
WHEN storage_profile_types.name ILIKE '%-SSD%' THEN '.ssd-m'
|
||||
WHEN storage_profile_types.name ILIKE '%-SAS' THEN '.sas-m'
|
||||
WHEN storage_profile_types.name ILIKE '%-SATA' THEN '.sata-m'
|
||||
ELSE '.['|| storage_profile_types.name ||']' END
|
||||
as code,
|
||||
storage_profile_types.name, -- *** это не тип, а профайл
|
||||
sum("limit")/1024./4 as gb_h_limit, -- 4 потому, что сбор раз в 15 минут
|
||||
sum(used)/1024./4 as gb_h_used
|
||||
from ngcloud_ru.capacity_storage
|
||||
join ngcloud_ru.vdc on capacity_storage.vdc_id=vdc.id
|
||||
join ngcloud_ru.tenant on tenant.id=vdc.tenant_id
|
||||
join ngcloud_ru.storage_profile_types on capacity_storage.storage_profile_types_id=storage_profile_types.id
|
||||
where tenant.wzcode = <cfqueryparam cfsqltype="cf_sql_integer" value=#tenant_wz_index#/>
|
||||
AND capacity_storage.timestamp >= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#ATTRIBUTES.dt_start#/>
|
||||
AND capacity_storage.timestamp < <cfqueryparam cfsqltype="cf_sql_timestamp" value=#ATTRIBUTES.dt_finish#/>
|
||||
group by tenant.wzcode, vdc.name, storage_profile_types.name
|
||||
order by tenant.wzcode, vdc.name, storage_profile_types.name;
|
||||
</cfquery>
|
||||
<cfquery name="qStorageAge" datasource="rpt">
|
||||
select max(capacity_storage.timestamp) as dt_load from ngcloud_ru.capacity_storage
|
||||
</cfquery>
|
||||
|
||||
<cfif (ATTRIBUTES.debug)>
|
||||
<cfdump var=#qStorage#/>
|
||||
<cfflush/>
|
||||
</cfif>
|
||||
|
||||
<!---
|
||||
"placement" "size_gb" "get_10k" "put_10k" "bytes_sent_gb"
|
||||
HOT_FREE_LIMIT 1 10 1 100
|
||||
--->
|
||||
<!--- надо дисконтировать не ГБ-часы а ГБ-месяцы --->
|
||||
<cfquery name="qS3Vol">
|
||||
select /* *** ниже дублирование кода агрегации - round ceil etc, обратить внимание при правке */
|
||||
(sum(bucket_stat.usage_rgw_main_size_actual)/<cfqueryparam cfsqltype="cf_sql_integer" value=#hours#/>) as vol_B
|
||||
,ceil(sum(bucket_stat.usage_rgw_main_size_actual)/<cfqueryparam cfsqltype="cf_sql_integer" value=#hours#/>/1024/1024/1024) as vol_GB
|
||||
,greatest(ceil(sum(bucket_stat.usage_rgw_main_size_actual)/<cfqueryparam cfsqltype="cf_sql_integer" value=#hours#/>
|
||||
/1024/1024/1024)-
|
||||
case when bucket_stat.placement_id = 1 then 1 else 0 end, 0) as vol_GB_free_tier_subtracted /*считается не в точности так, как в функции*/
|
||||
,case
|
||||
when bucket_stat.placement_id = 1 then 'paas.s3.ceph'
|
||||
when bucket_stat.placement_id = 2 then 'paas.s3.cphc'
|
||||
else '['|| TO_CHAR(bucket_stat.placement_id,'FM9') ||']' end as code
|
||||
--,bucket_stat.placement_id
|
||||
,'WZ'||lpad(bucket_stat.owner, 5, '0') as wz -- *** здесь не число, а строка, поэтому другая формула обратной сборки WZ
|
||||
|
||||
from s3billing.bucket_stat
|
||||
join s3billing.placement ON s3billing.placement.id=bucket_stat.placement_id
|
||||
join s3billing.bucket_info ON bucket_info.id=bucket_stat.bucket_id
|
||||
where bucket_stat.placement_id in (1,2)
|
||||
AND bucket_stat.owner = <cfqueryparam cfsqltype="cf_sql_varchar" value=#tenant_wz_index#/>
|
||||
AND bucket_stat.timestamp_addition >= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#ATTRIBUTES.dt_start#/>
|
||||
AND bucket_stat.timestamp_addition < <cfqueryparam cfsqltype="cf_sql_timestamp" value=#ATTRIBUTES.dt_finish#/>
|
||||
group by bucket_stat.owner, bucket_stat.placement_id
|
||||
order by bucket_stat.owner, bucket_stat.placement_id;
|
||||
</cfquery>
|
||||
|
||||
<cfif (ATTRIBUTES.debug)>
|
||||
<cfdump var=#qS3Vol#/>
|
||||
<cfflush/>
|
||||
</cfif>
|
||||
|
||||
<cfquery name="qS3VolAge">
|
||||
select max(timestamp_addition) as dt_load from s3billing.bucket_stat
|
||||
</cfquery>
|
||||
|
||||
<cfquery name="qS3OpsTrf">
|
||||
select
|
||||
sum(
|
||||
CASE WHEN
|
||||
(category.name LIKE 'get%'
|
||||
OR category.name LIKE 'head%'
|
||||
OR category.name LIKE 'options%'
|
||||
) THEN ops ELSE 0 END
|
||||
) as get
|
||||
,ceil(sum(
|
||||
CASE WHEN
|
||||
(category.name LIKE 'get%'
|
||||
OR category.name LIKE 'head%'
|
||||
OR category.name LIKE 'options%'
|
||||
) THEN ops ELSE 0 END
|
||||
)/10000) as get_10k
|
||||
,greatest(ceil(sum(
|
||||
CASE WHEN
|
||||
(category.name LIKE 'get%'
|
||||
OR category.name LIKE 'head%'
|
||||
OR category.name LIKE 'options%'
|
||||
) THEN ops ELSE 0 END
|
||||
)/10000) - CASE WHEN bucket_info.placement_id = 1 THEN 10 ELSE 0 END, 0
|
||||
) as get_10k_free_tier_subtracted
|
||||
|
||||
,sum(
|
||||
CASE WHEN
|
||||
(category.name LIKE 'put%'
|
||||
OR category.name LIKE 'post%'
|
||||
OR category.name LIKE 'patch%'
|
||||
OR category.name LIKE 'list%'
|
||||
) THEN ops ELSE 0 END
|
||||
) as put
|
||||
,ceil(sum(
|
||||
CASE WHEN
|
||||
(category.name LIKE 'put%'
|
||||
OR category.name LIKE 'post%'
|
||||
OR category.name LIKE 'patch%'
|
||||
OR category.name LIKE 'list%'
|
||||
) THEN ops ELSE 0 END
|
||||
)/10000) as put_10k
|
||||
,greatest(ceil(sum(
|
||||
CASE WHEN
|
||||
(category.name LIKE 'put%'
|
||||
OR category.name LIKE 'post%'
|
||||
OR category.name LIKE 'patch%'
|
||||
OR category.name LIKE 'list%'
|
||||
) THEN ops ELSE 0 END
|
||||
)/10000) - CASE WHEN bucket_info.placement_id = 1 THEN 1 ELSE 0 END, 0
|
||||
) as put_10k_free_tier_subtracted
|
||||
|
||||
,(sum(bytes_sent)) as bytes_sent
|
||||
,ceil(sum(bytes_sent)/1024/1024/1024) as bytes_sent_GB
|
||||
,greatest(ceil(sum(bytes_sent)/1024/1024/1024) - CASE WHEN bucket_info.placement_id = 1 THEN 100 ELSE 0 END, 0
|
||||
) as bytes_sent_GB_free_tier_subtracted
|
||||
|
||||
,case
|
||||
when bucket_info.placement_id = 1 then 'paas.s3.ceph'
|
||||
when bucket_info.placement_id = 2 then 'paas.s3.cphc'
|
||||
else '['|| TO_CHAR(bucket_info.placement_id,'FM9') ||']' end as code --*** зачем делать там ключ bigint
|
||||
,bucket_info.placement_id
|
||||
--category.name, bucket_info.name
|
||||
,'WZ'||lpad(user_info.name, 5, '0') as wz
|
||||
from s3billing.usage_bucket_by_user
|
||||
join s3billing.user_info on usage_bucket_by_user.user_id=user_info.id
|
||||
join s3billing.category on usage_bucket_by_user.category_id=category.id
|
||||
join s3billing.bucket_info on usage_bucket_by_user.bucketid=bucket_info.id
|
||||
join s3billing.placement on bucket_info.placement_id=placement.id
|
||||
where 1=1
|
||||
AND user_info.name = <cfqueryparam cfsqltype="cf_sql_varchar" value=#tenant_wz_index#/>
|
||||
AND usage_bucket_by_user.time >= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#ATTRIBUTES.dt_start#/>
|
||||
AND usage_bucket_by_user.time < <cfqueryparam cfsqltype="cf_sql_timestamp" value=#ATTRIBUTES.dt_finish#/>
|
||||
group by user_info.name, bucket_info.placement_id
|
||||
order by user_info.name, bucket_info.placement_id;
|
||||
</cfquery>
|
||||
|
||||
<cfif (ATTRIBUTES.debug)>
|
||||
<cfdump var=#qS3OpsTrf#/>
|
||||
<cfflush/>
|
||||
</cfif>
|
||||
|
||||
<cfquery name="qS3OpsTrfAge">
|
||||
select max(time) as dt_load from s3billing.usage_bucket_by_user
|
||||
</cfquery>
|
||||
|
||||
|
||||
<cfquery name="qUnifiedMetric" dbType="query">
|
||||
select wz, code || '.vcpu-m' as code, core_h as raw_metric, core_h as metric, core_h as chargeable_metric from qComputing
|
||||
union all
|
||||
select wz, code || '.ram-m' as code, gb_h as raw_metric, gb_h as metric, gb_h as chargeable_metric from qComputing
|
||||
union all
|
||||
select wz, code, gb_h_used as raw_metric, gb_h_used as metric, gb_h_used as chargeable_metric from qStorage
|
||||
union all
|
||||
select wz, code || '.vol-m', vol_b as raw_metric, vol_gb as metric, vol_GB_free_tier_subtracted as chargeable_metric from qS3Vol
|
||||
union all
|
||||
select wz, code || '.get-m', get as raw_metric, get_10k as metric, get_10k_free_tier_subtracted as chargeable_metric from qS3OpsTrf
|
||||
union all
|
||||
select wz, code || '.put-m', put as raw_metric, put_10k as metric, put_10k_free_tier_subtracted as chargeable_metric from qS3OpsTrf
|
||||
union all
|
||||
select wz, code || '.trf-m', bytes_sent as raw_metric, bytes_sent_gb as metric, bytes_sent_gb_free_tier_subtracted as chargeable_metric from qS3OpsTrf
|
||||
</cfquery>
|
||||
|
||||
<cfif (ATTRIBUTES.debug)>
|
||||
<cfdump var=#qUnifiedMetric#/>
|
||||
<cfflush/>
|
||||
</cfif>
|
||||
|
||||
<cfquery name="qCharge" dbType="query">
|
||||
SELECT
|
||||
<d:field_set titleMapOut="titleMap" lengthOut="fieldCount">
|
||||
<d:field title="Артикул">s.code</d:field>
|
||||
<d:field title="Услуга">s.svc</d:field>
|
||||
<d:field title="Компонент">s.component</d:field>
|
||||
<d:field title="Кол-во (метрика)" cfSqlType="CF_SQL_NUMERIC">m.chargeable_metric</d:field>
|
||||
<d:field title="Ед.изм.">s.unit</d:field>
|
||||
<d:field title="Цена &##8381; с НДС" cfSqlType="CF_SQL_NUMERIC">s.price*(100-s.discount)/100 as discounted_price</d:field>
|
||||
<d:field title="Стоимость &##8381; с НДС" cfSqlType="CF_SQL_NUMERIC">s.price*(100-s.discount)/100*m.chargeable_metric as charge</d:field>
|
||||
<!--- <d:field title="Дата НОУ" cfSqlType="CF_SQL_TIMESTAMP">s.dt_from</d:field> --->
|
||||
<!--- <d:field title="Дата оконч." cfSqlType="CF_SQL_TIMESTAMP">s.dt_to</d:field>
|
||||
<d:field title="Ключ строки">s.line_key</d:field> --->
|
||||
<d:field title="GPL с НДС" cfSqlType="CF_SQL_NUMERIC">s.price</d:field>
|
||||
<d:field title="Скидка%" cfSqlType="CF_SQL_NUMERIC">s.discount</d:field>
|
||||
<d:field title="Сырая метрика" cfSqlType="CF_SQL_NUMERIC">m.raw_metric</d:field>
|
||||
<d:field title="Приведенная метрика" cfSqlType="CF_SQL_NUMERIC">m.metric</d:field>
|
||||
|
||||
<!--- <d:field title="Клиентс. назв.">s.user_description</d:field> --->
|
||||
</d:field_set>
|
||||
FROM qSpec as s, qUnifiedMetric as m
|
||||
WHERE m.wz=<cfqueryparam cfsqltype="cf_sql_varchar" value=#request.auth.wz#/> AND s.code=m.code
|
||||
order by s.code
|
||||
</cfquery>
|
||||
|
||||
<cfset var report={}/>
|
||||
|
||||
<cfset report.qComputingAge = qComputingAge/>
|
||||
<cfset report.qStorageAge = qStorageAge/>
|
||||
<cfset report.qS3VolAge = qS3VolAge/>
|
||||
<cfset report.qS3OpsTrfAge = qS3OpsTrfAge/>
|
||||
<cfset report.qCharge = qCharge/>
|
||||
<cfset report.chargeTitleMap = titleMap/>
|
||||
<cfset report.chargeFieldCount = fieldCount/>
|
||||
|
||||
<cfset "CALLER.#ATTRIBUTES.output#" = report/>
|
||||
|
||||
<cfexit method="exittag"/>
|
||||
@@ -1,159 +0,0 @@
|
||||
<cfsilent>
|
||||
<cfimport prefix="m" taglib="lib"/>
|
||||
<cfimport prefix="c" taglib="lib/controls"/>
|
||||
<cfimport prefix="d" taglib="lib/data"/>
|
||||
<cfimport prefix="layout" taglib="layout"/>
|
||||
</cfsilent>
|
||||
|
||||
<m:prepare_ls entity="payg" accessObject="" pageInfoOut="pageInfo" trackOut="tr"/>
|
||||
|
||||
<cfparam name="dt_finish" type="date" default=#createDateTime(year(Now()),month(Now()),1,0,0,0)#/>
|
||||
<cfparam name="dt_start" type="date" default=#dateAdd('m',-1,dt_finish)#/>
|
||||
<cfset hours=dateDiff('h',dt_start,dt_finish)/>
|
||||
<cfset tenant_wz_index=val(mid(request.auth.wz,3,5))/><!--- cut off WZ --->
|
||||
<cfset tenant_wz_index="00169"/><!--- cut off WZ --->
|
||||
|
||||
|
||||
<cfif isDefined("output_xls")><cfmodule template="mod/payg.cfm"
|
||||
dt_finish=#dt_finish#
|
||||
dt_start=#dt_start#
|
||||
tenant_wz_index=#tenant_wz_index#
|
||||
output="report"
|
||||
debug=#isDefined("DEBUG")#
|
||||
/><layout:xml qRead=#report.qCharge# titleMap=#report.chargeTitleMap# filename="#pageInfo.entity#.xml"/>
|
||||
<cfabort/>
|
||||
</cfif><cfif isDefined("output_json")><cfmodule template="mod/payg.cfm"
|
||||
dt_finish=#dt_finish#
|
||||
dt_start=#dt_start#
|
||||
tenant_wz_index=#tenant_wz_index#
|
||||
output="report"
|
||||
debug=#isDefined("DEBUG")#
|
||||
/><!--- <cfdump var=#report#/><cfabort/> ---><layout:json qRead=#report.qCharge# titleMap=#report.chargeTitleMap# filename="#pageInfo.entity#.json"/>
|
||||
<cfabort/>
|
||||
</cfif><!---
|
||||
---><layout:page section="header" pageInfo=#pageInfo#>
|
||||
|
||||
<layout:attribute name="title">
|
||||
<cfoutput><b>Отчет по PAYG</b> #request.auth.wz#</cfoutput>
|
||||
</layout:attribute>
|
||||
<layout:attribute name="controls">
|
||||
<!---skip filter link, filter is not implemented--->
|
||||
<!---<layout:language_switch/>--->
|
||||
</layout:attribute>
|
||||
</layout:page>
|
||||
|
||||
<!--- Внимание! округление вверх при расчете метрик get, put S3. Расчетные суммы могут содержать погрешности округления, поэтому окончательный расчет стоимости (количество * цену) должен выполняться в бухгалтерской программе.
|
||||
<b>Если у клиента больше 1 действующего допника с услугами PAYG, то данные в отчете могут быть замножены, в этом случае считать вручную</b> --->
|
||||
|
||||
<cfoutput>
|
||||
<form method="post" action="">
|
||||
Период с <input type="text" name="dt_start" value="#dateFormat(dt_start,'YYYY-MM-DD')#"/>
|
||||
по <input type="text" name="dt_finish" value="#dateFormat(dt_finish,'YYYY-MM-DD')#"/>
|
||||
<input type="submit" style="cursor:pointer;"/>
|
||||
<!--- <input type="submit" name="DEBUG" value="DEBUG" style="cursor:pointer;"/> --->
|
||||
</form>
|
||||
|
||||
|
||||
|
||||
<cfflush/>
|
||||
|
||||
<cfmodule template="mod/payg.cfm"
|
||||
dt_finish=#dt_finish#
|
||||
dt_start=#dt_start#
|
||||
tenant_wz_index=#tenant_wz_index#
|
||||
output="report"
|
||||
debug=#isDefined("DEBUG")#
|
||||
/>
|
||||
|
||||
Часов в периоде: <b>#hours#</b> Строк в отчете: <b>#report.qCharge.recordCount#</b><br/>
|
||||
Актуальность данных:
|
||||
VCD Computing: <b>#dateFormat(report.qComputingAge.dt_load,'YYYY-MM-DD')# #timeFormat(report.qComputingAge.dt_load,'HH:MM:SS')#</b>
|
||||
VCD Storage: <b>#dateFormat(report.qStorageAge.dt_load,'YYYY-MM-DD')# #timeFormat(report.qStorageAge.dt_load,'HH:MM:SS')#</b>
|
||||
S3 хранение: <b>#dateFormat(report.qS3VolAge.dt_load,'YYYY-MM-DD')# #timeFormat(report.qS3VolAge.dt_load,'HH:MM:SS')#</b>
|
||||
S3 операции и трафик: <b>#dateFormat(report.qS3OpsTrfAge.dt_load,'YYYY-MM-DD')# #timeFormat(report.qS3OpsTrfAge.dt_load,'HH:MM:SS')#</b>
|
||||
|
||||
<br/>
|
||||
<a href="#request.thisPage#?output_xls" title="экспорт в Excel" style="margin-left:.5em; height:100%;" target="_blank"><img src="img/xls.gif" style="vertical-align:text-bottom;"/></a>
|
||||
|
||||
<a href="#request.thisPage#?output_json" title="экспорт в json" style="margin-left:.5em; height:100%;" target="_blank"><img src="img/json.svg" style="vertical-align:text-bottom;" width="13" height="13"/></a>
|
||||
</cfoutput>
|
||||
<table class="worktable">
|
||||
<thead>
|
||||
<td width="1%"></td>
|
||||
<!--- <th width="3%">WZ</th> --->
|
||||
<th width="7%">Артикул</th>
|
||||
<th width="7%">Кол-во (метрика)</th>
|
||||
<th width="3%">Ед.изм.</th>
|
||||
<th width="3%">Цена ₽ со скид., с НДС</th>
|
||||
<th width="3%">Стоимость ₽ с НДС</th>
|
||||
|
||||
<td width="3%"> </td>
|
||||
|
||||
<td width="10%">Услуга</td>
|
||||
<td width="10%">Компонент</td>
|
||||
<!--- <td width="10%">Клиентс. назв.</td> --->
|
||||
|
||||
<td width="3%">GPL с НДС</td>
|
||||
<td width="3%">Скидка%</td>
|
||||
|
||||
<!--- <td width="6%">Дата НОУ</td>
|
||||
<td width="6%">Дата оконч.</td>
|
||||
|
||||
<td width="3%">Ключ строки</td> --->
|
||||
<!--- <td width="3%">Сырая метрика</td> --->
|
||||
<td width="3%">Оплачиваемый объем</td>
|
||||
|
||||
<!--- <td width="5%">Договор</td>
|
||||
<td width="10%">Допник</td>
|
||||
<td width="10%">Сделка</td> --->
|
||||
|
||||
</thead>
|
||||
|
||||
<cfoutput query="report.qCharge" group="svc">
|
||||
<cfset var acc=0/>
|
||||
<tr>
|
||||
<td bgcolor="##ccc"></td>
|
||||
<td bgcolor="##ccc" colspan="5" class="b"> #svc#</td>
|
||||
<td></td>
|
||||
<td bgcolor="##ccc" colspan="99"></td>
|
||||
</tr>
|
||||
<cfoutput>
|
||||
<cfset acc=acc+charge/>
|
||||
<tr>
|
||||
<td>#currentRow#</a></td>
|
||||
|
||||
<td>#code#</td>
|
||||
<td class="r" style="font-size:120%; padding:0 1em;">#chargeable_metric#</td>
|
||||
<td class="c" style="font-size:90%;">#unit#</td>
|
||||
<td class="r">#discounted_price#</td>
|
||||
<td class="r">#charge#</td>
|
||||
|
||||
<td></td>
|
||||
|
||||
<td>#svc#</td>
|
||||
<td>#component#</td>
|
||||
<!--- <td>#user_description#</td> --->
|
||||
|
||||
<td class="r">#price#</td>
|
||||
<td class="r">#discount#</td>
|
||||
|
||||
<td class="r">#metric#</td>
|
||||
|
||||
</tr>
|
||||
</cfoutput>
|
||||
<tr>
|
||||
<td bgcolor="##eee" style="border-bottom:2px solid gray;"></td>
|
||||
<td bgcolor="##eee" colspan="4" class="r" style="border-bottom:2px solid gray;"><b>Итого</b> #svc#:</td>
|
||||
<td bgcolor="##eee" colspan="1" class="r b" style="border-bottom:2px solid gray;">#NumFmt(acc,2)#</td>
|
||||
<td colspan="14" style="border-bottom:2px solid gray;"></td>
|
||||
</tr >
|
||||
</cfoutput>
|
||||
|
||||
</table>
|
||||
<!--- <cfdump var=#qCharge#/> --->
|
||||
|
||||
|
||||
<layout:page section="footer"/>
|
||||
|
||||
<!--- select * from s3billing.billing_per_user_fix_pl('2025-05-01 00:00:00','2025-06-01 00:00:00','1395'); --->
|
||||
|
||||
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
@@ -1,47 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Creator: CorelDRAW X8 -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="78.6273mm" height="70.4009mm" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
|
||||
viewBox="0 0 37746 33797"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<style type="text/css">
|
||||
<![CDATA[
|
||||
.fil3 {fill:none}
|
||||
.fil2 {fill:#D7F372}
|
||||
.fil4 {fill:#FEFEFE;fill-rule:nonzero}
|
||||
.fil5 {fill:#638328;fill-rule:nonzero}
|
||||
.fil1 {fill:url(#id1)}
|
||||
.fil0 {fill:url(#id2)}
|
||||
]]>
|
||||
</style>
|
||||
<clipPath id="id0">
|
||||
<path d="M20432 0c8718,-38 15581,2266 15458,9209 -25,787 -197,745 -1206,434 -5376,-1662 -8578,-3016 -12410,-6248 -1067,-900 -1945,-1797 -2743,-2837 -478,-648 -8,-545 901,-558zm-18416 19428c1785,-4186 2803,-6725 6956,-9214 1814,-1088 3728,-1795 5744,-2401 5011,-1469 9917,-953 14495,540 1697,562 2833,1208 4123,2158 602,450 989,178 1872,-71 402,-113 1572,-529 1912,-305 933,615 -675,1120 -1291,1389 -1798,785 -1256,251 -1265,1780 -43,7030 -7302,11174 -13250,12374 -3409,688 -7029,1146 -9343,1499 -5916,900 -5060,-1185 -9071,-2819 -937,-381 -2531,-341 -2879,-1361 -223,-655 1540,-2751 1997,-3569z"/>
|
||||
</clipPath>
|
||||
<linearGradient id="id1" gradientUnits="userSpaceOnUse" x1="-740.954" y1="15224.8" x2="37478.6" y2="11458.4">
|
||||
<stop offset="0" style="stop-opacity:1; stop-color:#C6C517"/>
|
||||
<stop offset="1" style="stop-opacity:1; stop-color:#E7D900"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="id2" gradientUnits="userSpaceOnUse" x1="-442.19" y1="20004.6" x2="37830.6" y2="16055.8">
|
||||
<stop offset="0" style="stop-opacity:1; stop-color:#89A423"/>
|
||||
<stop offset="1" style="stop-opacity:1; stop-color:#638429"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g id="Layer_x0020_1">
|
||||
<metadata id="CorelCorpID_0Corel-Layer"/>
|
||||
<path class="fil0" d="M22767 31689l-14060 2085c-2226,330 -4572,-2956 -6349,-3695 -1559,-649 -1653,-822 -1915,-2004 -266,-1196 -259,-3137 -401,-4460 -150,-1391 203,-1130 973,-2123 591,-795 964,-1292 1359,-2228 1413,-3396 2313,-5352 5285,-7695 3781,-2753 8236,-4067 12877,-4299 774,-89 484,-275 112,-747 -812,-1079 -1165,-2747 -1381,-4068 -68,-416 -308,-2197 198,-2372 228,-79 1093,72 1388,100 101,9 200,16 301,22 1094,51 2280,-41 2615,-17 4852,417 10503,1550 11899,7016 226,935 209,1325 200,2255 -2,142 -8,1026 126,1086 215,97 947,-1057 1467,133 110,344 145,764 169,1126 8,120 17,237 29,356 91,880 115,1755 53,2638 -23,319 -23,817 -223,1077 -611,795 -1576,1292 -2268,2065 -435,506 -635,1974 -834,2703 -720,2633 -1869,4361 -3594,4590l-7936 1054 -90 5402z"/>
|
||||
<path class="fil1" d="M20432 0c8718,-38 15581,2266 15458,9209 -25,787 -197,745 -1206,434 -5376,-1662 -8578,-3016 -12410,-6248 -1067,-900 -1945,-1797 -2743,-2837 -478,-648 -8,-545 901,-558zm-18416 19428c1785,-4186 2803,-6725 6956,-9214 1814,-1088 3728,-1795 5744,-2401 5011,-1469 9917,-953 14495,540 1697,562 2833,1208 4123,2158 602,450 989,178 1872,-71 402,-113 1572,-529 1912,-305 933,615 -675,1120 -1291,1389 -1798,785 -1256,251 -1265,1780 -43,7030 -7302,11174 -13250,12374 -3409,688 -7029,1146 -9343,1499 -5916,900 -5060,-1185 -9071,-2819 -937,-381 -2531,-341 -2879,-1361 -223,-655 1540,-2751 1997,-3569z"/>
|
||||
<g style="clip-path:url(#id0)">
|
||||
<g id="_3107826610240">
|
||||
<path id="1" class="fil2" d="M32620 15645c91,-46 98,-37 3,-91 -66,80 -20,59 -3,91zm-9180 173l399 13 94 3 24 -102c121,-34 39,27 76,-81l-104 -24c-76,-2 -129,-55 -130,-59 -3,-10 -11,-11 -18,-15l-68 -4c-94,14 -151,38 -213,69l-167 12 -8 120 12 65 103 3zm3362 -16369c31,57 -35,35 43,50 123,23 4,11 64,-18 -27,-64 39,-43 -41,-56 -131,-22 -7,-12 -66,24zm10290 3584c-14,-80 10,-36 -56,-77 -2,4 -94,5 -19,84 51,53 74,-7 75,-7zm2277 11808c100,-46 108,-71 4,-122 -65,81 -40,56 -4,122zm-598 -13384c72,-96 70,-72 -25,-113 -75,90 -74,70 25,113zm-486 7658c-47,-62 -81,7 -83,7 65,99 -66,22 64,80 3,-6 100,22 19,-87zm-14361 -9918c35,-131 44,-89 -39,-138 -4,7 -53,-26 -31,67 27,121 -23,26 70,71zm13953 15010c7,-87 -6,-35 -58,-63 -74,84 -46,57 -4,129 45,-22 53,54 62,-66zm1320 -15163c106,-113 26,-91 4,-126 -51,29 -67,-45 -62,71 4,73 20,40 58,55zm2646 15025c-2,-2 9,-68 -88,-57 -109,12 -37,7 -73,64 105,50 64,60 161,-7zm-292 -3726c48,-52 47,-33 49,-171 -84,-2 -93,-38 -195,78 -83,11 -33,-18 -92,47l115 91c47,-29 48,-35 123,-45zm-2986 3302c-113,22 -40,12 -65,57 182,135 184,-23 184,-24 -10,-6 19,-60 -119,-33zm-1300 1033c-76,-44 -26,-43 -193,-4 18,157 33,163 111,169 166,12 14,18 79,-36l3 -129zm-28 818c-15,-28 43,-120 -226,-46 35,72 -28,65 93,92 164,37 124,-39 133,-46zm-7887 -16516l182 4c21,-158 -15,-177 -161,-176l-21 172zm1556 1675l148 114c99,86 124,57 174,-16 -50,-145 -12,-36 -146,-105 -44,-243 65,-253 -76,-325 -82,93 -25,15 -43,187 -15,145 -10,6 -57,145zm-16717 6826c58,-32 47,42 53,-46 7,-83 4,-30 -50,-59l-24 38c3,61 -11,24 21,67zm-1336 -2651c25,-6 44,9 63,-22 77,-123 10,-42 -8,-80 -80,40 -47,5 -55,102zm-4579 -6954l-112 -3 26 95c101,-38 42,50 86,-92zm7494 3833c33,-24 94,21 33,-122 -79,33 -49,-52 -62,48 -20,150 -16,19 29,74zm14390 12465c-110,-30 -18,-3 -62,20 24,35 -18,154 116,33 -22,-35 -5,-40 -54,-53zm-16336 1107c44,-72 -19,-62 -22,-69 -108,42 -55,-27 -72,102 120,-4 51,36 94,-33zm-15555 -8727c0,-1 54,27 19,-104 -41,1 -53,-32 -83,34 -36,76 49,66 64,70zm29578 -7130c115,91 90,22 123,-26 -55,-41 62,-61 -72,-36 -85,16 -30,37 -51,62zm-15613 10133c8,-12 163,46 3,-107 -158,125 -4,106 -3,107zm19829 6238c-113,-3 -59,-5 -148,25 75,104 35,82 146,96l2 -121zm-1810 -750c-9,127 22,73 56,93 13,-18 98,-37 6,-177 -40,23 -55,-4 -62,84zm-22909 -1701c110,-49 103,-21 88,-170l-170 17 -8 103 29 61 61 -11zm17260 -7459c57,-32 46,52 67,-70 25,-145 -10,-61 -33,-93 -62,23 -49,-63 -69,61 -24,151 20,79 35,102zm6292 8218c-86,10 -54,14 -87,55 20,17 25,52 112,48 130,-7 69,-26 89,-71 -34,-23 -5,-44 -114,-32zm-18726 -12582l138 126c48,-78 33,-47 37,-156 -115,-27 -88,-44 -175,30zm5425 2089c112,8 43,39 121,-26l69 -111c-57,-38 50,-12 -63,-36 -108,-23 -8,-17 -111,3l-16 170zm199 -5155c68,-15 102,32 91,-91 -10,-106 -55,-86 -91,-103 -91,200 -13,173 0,194zm-4498 9344c44,-116 -2,-63 -8,-78 -188,-98 -154,47 -70,159 12,-9 44,9 78,-81zm-14595 -8298l-37 -85 -225 -25c13,84 -5,39 37,90 11,13 11,8 29,27 101,111 -29,62 100,79 134,17 77,-16 96,-28l0 -58zm16516 14224c-51,-40 -1,-28 -113,-50 -41,-8 -243,-20 -290,67 61,129 108,18 237,86 55,29 -10,50 104,69 198,32 132,-42 146,-52 -49,-181 23,-57 -89,-111l5 -9zm-15097 58c48,-69 44,-72 16,-145 -273,51 -22,42 -250,167 31,-165 -35,-99 -48,-114l-10 228 209 8 83 -144zm10508 -4775c26,-191 49,60 -34,-54 -80,69 -76,58 -4,123 52,-55 25,22 38,-69zm2333 -7579c33,-5 125,10 56,-109 -99,52 -45,-60 -56,109zm-3430 -2611c31,53 -29,36 45,50 112,21 9,9 62,-17 -23,-52 62,-39 -40,-56 -76,-13 -10,-11 -67,23zm-5194 1548c20,-171 28,-57 -48,-102 -23,40 -71,-39 -25,67 24,53 -2,25 73,35zm2622 14176c16,-129 5,-16 -32,-67 -96,48 -105,68 -4,128 34,-41 21,52 36,-61zm20631 -662c17,-119 -2,-25 -34,-66 -97,47 -110,67 -4,121 35,-34 24,49 38,-55zm-26456 -15122c101,69 78,57 133,-25 -30,-22 47,-55 -72,-37 -124,19 -29,27 -61,62zm-3047 11843c12,19 -43,82 103,51 57,-13 47,-31 63,-80 -75,-40 -83,-41 -168,-21l2 50zm590 1476c-68,85 -67,86 -5,161 100,-47 96,-110 5,-161zm9676 2249c132,-45 120,-6 96,-144 -185,-46 -50,-16 -91,31 11,22 4,32 23,36 49,12 16,9 52,-9 -39,-41 -1,-18 -75,-27 -8,6 -42,26 -44,28 -15,11 -32,39 -36,33 -3,-7 -12,13 -19,19l7 24c-83,-5 -17,-34 -119,21l-151 150c-7,14 -26,96 -30,118 184,-1 47,-2 195,-98 58,-38 107,-51 184,-77l17 -88 -9 -17zm-10949 -16354c55,-82 68,-123 -24,-178 -4,5 -49,-31 -45,89 4,131 35,74 69,89zm5381 2060c95,56 114,57 192,-23 -73,-75 -188,-91 -192,23zm4150 14183c1,-1 64,-22 50,-106 -19,-117 0,-36 -73,-72 -30,39 -121,103 23,178zm17483 -2004c-59,-80 -103,-72 -188,-35 18,24 -70,68 84,81 141,12 94,-39 104,-46zm-13680 -1675c141,42 169,46 247,-29 -37,-39 -45,-69 -145,-59 -99,10 -73,22 -102,88zm-8064 1221l-36 23c-90,81 -54,49 -56,120 261,25 188,-170 188,-171 -115,-7 34,-13 -60,2l-36 26c7,17 -3,42 39,48 3,1 16,-4 19,-4 5,0 15,-7 23,-10 -39,-35 6,-19 -81,-34zm8633 -1952c-7,-10 54,-79 -137,-26 9,87 13,131 90,196 33,-28 19,-1 55,-52 24,-33 -22,88 23,-39 49,-141 4,-71 -31,-79l-78 -1 25 81 53 -80zm-7088 2769c-11,-74 -88,-139 -148,-163l-43 119 162 129c15,-21 48,44 29,-85zm-7692 303l-2 61 261 28c11,-216 -161,-128 -259,-89zm39584 -10253c14,28 -26,64 65,64 135,1 27,20 78,-60 -44,-33 11,-44 -75,-39 -129,8 -18,-2 -68,35zm419 750c28,-36 34,52 41,-69 10,-164 -9,-60 -36,-101 -30,17 -56,-27 -71,73 -20,138 30,78 66,97zm3066 2473c55,-72 68,-95 66,-198 -92,-19 -136,-38 -155,50 -18,87 89,148 89,148zm2396 -3620c57,42 -52,30 106,49 159,20 20,7 113,-14 -22,-95 28,-126 -112,-132 -102,-5 -86,13 -107,97zm-718 4372l-51 -56c213,-116 92,-238 -70,-239 -152,-1 -176,38 -165,195l14 39c15,128 95,141 227,140l45 -79zm-11882 -7680c96,59 33,27 76,3l-58 -18c-3,-8 -12,11 -18,15zm-21198 15445c35,43 20,47 60,29 5,-2 14,-15 22,-22l-82 -7zm5007 1132l97 3 -6 -58c-40,-60 5,-36 -76,-65 -35,-12 -55,0 -88,1l5 117 68 2zm19326 -14421l-61 0c-35,54 -28,-70 -32,67 -4,136 11,60 30,93 73,-19 31,14 65,-50 146,-35 47,16 96,-68 -57,-55 15,-32 -98,-42zm-8429 13335c39,-6 206,-169 -164,-173 -70,95 -80,76 -102,170l266 3zm14571 -9261c47,74 57,123 127,4 -116,-124 -105,-20 -127,-4zm909 -1194c55,84 170,131 199,-23 -26,-14 -10,-39 -115,-26 -57,8 -60,10 -84,49zm271 2163c-1,-1 34,-56 -85,-37 -107,17 -28,-8 -84,61 38,68 -52,70 107,81 68,5 70,-17 106,-46 -12,-97 2,-33 -44,-59l-117 15 83 62c43,-50 20,-9 34,-77zm2227 -30c105,19 81,28 165,-11 -16,-86 -60,-131 -144,-155 -48,81 -43,60 -21,166zm-680 -1938c36,-37 13,13 35,-54 59,-184 69,-195 19,-219 -7,-2 -19,-13 -22,-4 -3,7 -75,-30 -95,109 -15,102 -6,135 63,168zm-7887 -1031c174,9 174,-15 198,-181l-195 0 -3 181zm-434 -3677c0,-132 -97,-207 -219,-204 -14,163 61,224 219,204zm9067 10670l-228 -20c15,143 32,200 191,182 20,-35 15,-15 29,-68 0,-1 7,-80 8,-94zm-11458 -8654l180 15c41,-77 53,-78 67,-187l-240 -2 -7 174zm-13484 -1787c67,65 -100,40 38,31 70,-5 24,3 66,-28 -77,-64 98,-40 -43,-30 -87,6 4,-25 -61,27zm15318 6420c8,8 -37,96 94,3 -76,-59 111,-37 -40,-27 -71,5 -10,-11 -54,24zm-4361 -5494c0,-176 44,-57 -80,-98 19,109 -24,65 80,98zm5777 11913c-52,-67 -18,-71 -104,-32 75,108 -101,-50 19,46 3,3 11,10 15,12 122,59 35,-5 70,-26zm452 -9192c-16,-105 17,-46 -64,-89 -51,58 -31,28 -37,112l0 24c139,141 57,117 171,135l19 -142 -89 -40zm1300 -184c26,36 -55,67 176,58 3,-16 70,-189 -107,-115 -68,28 -31,-2 -69,57zm-6645 5321c35,-145 -23,-89 -32,-106 -123,17 -84,-60 -108,72 -22,127 47,87 73,100 61,-30 38,56 67,-66zm12539 -5533c161,1 121,36 140,-122 -54,-35 -31,-46 -160,-30 -8,73 -19,77 20,152zm-7898 4642l158 -3 13 -151 -160 -4 -11 158zm-17042 -7038c56,111 45,115 104,4 -52,-38 -53,-32 -104,-4zm9439 14921c154,-74 34,-104 32,-108 -148,63 -33,108 -32,108zm-15492 -14617c77,-131 38,-84 -75,-106 31,146 -22,74 75,106zm2595 5412c115,-55 112,-64 4,-122 -16,20 -39,-42 -33,60 3,61 1,30 29,62zm3716 -5475c14,118 -32,80 92,86 38,-95 6,-127 -92,-86zm-11754 7101c6,-101 5,-86 -98,-105 -13,127 -2,118 98,105zm5801 -8233c54,-27 68,45 62,-68 -3,-75 -27,-45 -58,-61 -2,3 -85,14 -4,129zm-10835 7881c-39,-69 -44,-121 -135,-4 60,52 -67,23 67,40 113,15 27,-3 68,-36zm25397 -740c-39,10 -173,13 -98,95 96,105 86,-47 98,-95zm-2065 8351c-44,-72 23,-10 -75,-89 -65,-51 0,-9 -75,-42l-42 107 31 161c92,9 87,6 142,-32l19 -105zm-14954 -14214c87,52 55,48 150,6 -45,-139 -121,-139 -150,-6zm2674 8592c-160,2 -83,47 -93,81l164 16c3,-66 28,-97 -71,-97zm-12857 -11651c60,116 145,73 198,6 -88,-66 -119,-55 -198,-6zm14730 9603c8,31 -35,70 85,81 110,11 71,-14 85,-76 -83,-43 -68,-37 -170,-5zm-1945 -4959c-114,-51 -60,-57 -140,3l-4 58c93,64 166,73 228,-21 -41,-48 23,-26 -84,-40l-116 0c37,83 -28,35 83,68 54,-39 15,12 33,-68zm1791 10456c-76,-57 -141,-72 -205,22 16,13 16,57 118,48 109,-8 68,-30 87,-70zm6791 -1117c36,-46 36,70 46,-94 8,-128 -24,-68 -40,-87 -41,17 -64,-27 -71,84 -7,141 28,77 65,97zm-19845 -1831c156,4 126,-56 115,-191 -148,46 -119,6 -115,191zm28588 2560c110,14 138,-57 145,-166l-129 10c-85,158 -16,155 -16,156zm-24535 1960c-95,25 -67,45 -80,133 123,4 100,21 190,-35 -15,-82 23,-133 -110,-98zm-1340 -12555c34,-29 35,29 35,-82 -176,-85 -220,125 -198,245 233,90 139,21 170,-18l-9 -56 2 -89zm-138 10191c7,-3 15,-9 19,-11 187,-98 143,-30 160,-133 -81,-49 -97,-61 -235,49 -9,7 -14,11 -18,14l-10 -4 3 8c-6,4 -7,6 -36,40 48,43 -12,16 62,44 104,41 37,-1 55,-7zm8367 -12746l142 -3 17 -140c-107,-42 -56,-44 -174,-39 -39,107 -19,70 15,182zm13541 9864c19,-155 31,-118 -58,-202l-114 -68 -18 135c5,4 38,33 47,42 111,117 -30,57 143,93zm988 4753c45,-102 30,10 23,-135l-34 -127c-82,44 -10,4 -55,41l-47 40c-59,65 -45,-8 -44,128 63,55 64,54 157,53zm12225 -12623c69,-36 48,37 60,-51 11,-77 1,-29 -27,-74 -74,34 -47,-58 -61,47 -12,85 -16,23 28,78zm-541 532c79,-38 56,43 72,-73 18,-134 -33,-95 -37,-102 -62,28 -51,-64 -71,68 -26,164 18,78 36,107zm3249 -2408c42,-47 40,67 48,-70 9,-158 -18,-67 -43,-99 -62,25 -63,-61 -81,69 -20,138 47,86 76,100zm1303 9111c11,10 -39,54 99,53 166,-1 78,-27 103,-76 -9,-7 35,-50 -103,-50 -165,1 -70,41 -99,73zm-491 -3647c91,-9 105,-16 198,-88 -55,-161 -128,-160 -199,-171 8,167 44,104 1,259zm-24692 -3316c89,44 53,43 105,-26 -38,-27 54,-78 -66,-29 -4,1 -13,8 -16,10l-23 45zm13671 13200c-34,-129 31,-90 -104,-104 13,123 -20,66 104,104zm-2531 -392c-27,-46 15,-108 -118,-32 19,31 -50,43 47,68 133,33 20,3 71,-36zm7334 520c-76,-6 -56,36 -71,58 59,45 -67,20 67,36 129,15 17,4 69,-31 -26,-54 52,-54 -65,-63zm-2346 -1174c-22,-102 14,-11 -61,-51 -44,43 -78,-66 -39,94 19,78 18,22 64,42 41,-48 53,-4 36,-85zm-2672 1531c-126,3 -69,18 -100,66 34,27 41,99 222,7 -23,-40 -10,-76 -122,-73zm2389 -1391c-144,-10 -215,11 -139,175l166 10 -27 -185zm-13510 -6878c31,-55 21,-40 3,-86 -56,59 -65,-88 -32,42 34,130 -97,-14 29,44zm11036 7899c51,38 -7,20 62,28 135,17 -25,23 42,-24 -37,-35 4,-24 -62,-33 -161,-23 46,-43 -42,29zm-26597 -10879c62,73 10,58 96,32 -17,-44 72,-25 -23,-45 -72,-14 -23,-8 -73,13zm24877 10019c-13,-82 1,-14 -47,-36 -75,86 -26,63 26,92 24,-39 30,7 21,-56zm-13301 -13607c15,-24 58,33 23,-60 -25,-65 14,-8 -49,-43 -39,74 -63,63 26,103zm-1474 10770c72,-89 63,-70 -26,-109 -101,108 -21,86 26,109zm2490 -9768c5,-7 104,11 5,-148 -44,51 -53,-76 -44,73 6,81 4,33 39,75zm-455 582l45 -26c-21,-110 32,-69 -101,-93 -6,111 -40,72 56,119zm-2645 4875c5,-126 -3,-96 -98,-104 -11,117 -39,123 98,104zm-15387 -7335c18,35 -39,43 62,61 137,25 73,-13 98,-27 -5,-10 32,-155 -160,-34zm3775 14905c-5,113 34,83 37,87 96,-48 93,-123 5,-181 -22,29 -35,-53 -42,94zm16124 -7293c1,0 19,130 175,5 -14,-31 12,-58 -77,-63 -125,-7 -71,37 -98,58zm4169 4706c36,-17 59,39 73,-89 14,-124 -31,-78 -39,-88 -47,21 -58,-27 -69,83 -13,129 26,80 35,94zm-14149 1449c-78,-12 -84,28 -88,128l195 -3c-12,-79 -17,-110 -107,-125zm-10285 -377c144,13 156,25 270,-28 -59,-65 -141,-121 -249,-121l-21 149zm30877 -12750c85,40 49,47 105,-26 -20,-16 24,-117 -105,26zm-4545 -547c57,-82 74,-71 -25,-119 -65,93 -66,76 25,119zm6390 36c29,-122 42,-90 -84,-116 14,175 -38,71 84,116zm5154 2987c-1,45 -19,42 30,111 107,-34 39,71 81,-92 -1,-4 -13,-87 -13,-87 -16,-49 -13,-36 -31,-65 -83,37 -49,-29 -67,133zm-7078 -529c164,-26 122,59 107,-100 -108,-4 -71,-31 -107,100zm1289 -911c4,-125 47,-86 -67,-119 -93,100 -38,96 67,119zm2389 12606c-126,16 -58,-23 -117,61 78,102 120,95 117,-61zm-6679 -14484c93,-49 101,-26 33,-126 -50,15 -41,-45 -66,34 -45,140 4,62 33,92zm-6632 -463c15,26 -10,57 69,74 138,32 72,-15 95,-30 -35,-82 47,-48 -65,-68 -29,-6 -93,19 -99,24zm9162 3323c39,-11 64,56 82,-82 14,-103 -25,-89 -47,-113 -98,45 -81,123 -35,195zm11121 7323c77,-41 52,57 68,-98 6,-49 -11,-65 -20,-97 -60,22 -76,-44 -92,91 -17,134 36,91 44,104zm-6944 6433l64 2c35,-109 52,-107 -11,-201 -29,8 -65,-56 -82,91 -8,64 4,48 29,108zm-9851 -12707c33,-80 14,-192 -50,-252 -80,-76 -14,-26 -117,-47 51,222 26,39 90,163 44,86 -28,59 77,136zm17007 11129c-5,-4 -12,-10 -18,-14l-82 -58c-96,106 -9,225 150,201 10,-144 19,-76 -50,-129zm-14523 -7357l6 118 177 17 4 -157c-119,-17 -93,-25 -187,22zm1634 621c122,-10 52,43 137,-39 82,-80 30,-20 53,-141l-179 10 -11 170zm4069 -5651l169 46c4,-99 16,-85 -35,-146 -85,-102 -17,-50 -154,-73l20 173zm10176 12053c-10,-85 13,-6 -49,-52 -38,41 -37,-9 -30,69 9,97 -12,14 55,43 37,-56 37,58 24,-60zm-8682 -13172c37,61 -40,39 51,54 69,12 24,3 66,-21 -13,-32 49,-134 -117,-33zm2107 1274c32,-19 113,33 33,-122 -78,35 -50,-47 -63,48 -17,129 -11,23 30,74zm-2180 -3663l-108 -3c2,174 -2,49 42,106 98,-52 42,59 66,-103zm10237 6623c21,-30 58,43 35,-71 -21,-101 17,-22 -68,-66 -52,93 -66,71 33,137zm-4681 8279c36,-113 -28,-91 -30,-94 -79,34 -36,-30 -61,65 -8,29 10,72 15,95 50,-25 47,25 76,-66zm1712 -10475c91,-11 72,27 106,-40 54,-109 -37,-81 -62,-94l-37 44c-16,57 -18,8 -7,90zm-2158 653c14,19 -16,66 79,65 118,-2 32,5 75,-60 -18,-16 33,-129 -154,-5zm-9161 -4516c140,63 104,33 149,-46 -10,-8 40,-74 -77,-47 -186,44 -11,10 -72,93zm14791 10390c-148,-15 -74,13 -105,64 54,49 -82,47 102,53 102,4 98,-46 99,-47 -19,-29 7,-60 -96,-70zm-7396 -6688c36,71 -84,46 67,61 7,-6 18,-26 21,-20l74 -89c49,-90 34,-58 -1,-120 -66,20 -27,3 -73,42 -4,4 -10,10 -14,14 -4,4 -10,10 -14,14 -5,5 -12,11 -16,16l-35 33 -9 49zm3325 -2979c18,41 -50,73 94,76 129,2 71,-21 95,-70 -6,-6 38,-52 -90,-53 -164,-1 -55,12 -99,47zm-4326 1161l115 0c21,-126 29,-84 -30,-190 -116,49 -118,87 -85,190zm5264 1728c3,-4 48,17 51,-93 3,-169 -3,-47 -45,-100 -39,16 -76,-51 -89,107 -7,77 16,64 83,86zm3737 8908c127,-174 17,-185 6,-201 -65,17 -98,-21 -96,100 3,114 51,86 90,101zm-578 -7651c-69,-84 28,-32 -83,-63 -47,-14 -89,-4 -137,1 12,162 117,193 220,62zm-10792 -2729c100,-7 112,56 202,-184l-175 3 -27 181zm11216 4612c69,-72 57,-54 46,-178l-200 -7c-4,7 -10,9 -11,18l-76 95c135,22 67,-7 160,87l81 -15zm-25274 1701c77,60 7,56 87,3 -48,-47 101,-40 -34,-29 -73,6 -3,-17 -53,26zm-2320 -4177c8,14 14,124 93,3 -55,-40 95,-28 -36,-22 -55,3 -22,-1 -57,19zm3531 6166c27,41 -51,47 54,45 61,-2 16,-6 40,-42 -34,-25 -1,-17 -55,-23 -3,0 -17,14 -19,7 -3,-7 -13,9 -20,13zm9742 -9188c141,-30 64,23 96,-75 -192,-10 -27,10 -96,75zm-816 7909c14,-101 12,5 -30,-35 -126,99 -4,94 -3,96 46,-55 21,17 33,-61zm4718 4713c-57,112 -6,23 20,71 19,-13 146,-53 23,-91 -70,-21 -30,-5 -43,20zm-8122 -431c-16,-86 16,-10 -58,-43 -83,124 13,93 34,107 35,-58 47,63 24,-64zm17489 -3007c175,99 121,19 148,5 -163,-141 -131,-17 -148,-5zm-22342 4930c132,-4 153,38 191,-77l-183 -33c-126,37 -56,-3 -107,63 48,51 -83,44 99,47zm12980 -12068c-5,14 -15,15 -16,31 -2,16 -12,19 -13,27 -6,30 -6,60 -4,92l176 14 5 -153 -148 -11zm3361 -432c36,-54 25,-37 3,-87 -60,72 -60,-95 -28,43 33,144 -77,-37 25,44zm-4106 70c11,-16 62,31 29,-65 -27,-79 22,-7 -54,-51 -28,44 -74,-63 -27,70 23,65 -11,17 52,46zm4107 -1014c84,-42 48,57 62,-54 19,-151 13,-10 -29,-78 -90,38 -52,-66 -66,48 -12,105 -22,20 33,84zm5309 -1291c25,-37 79,60 36,-83 -31,-104 27,-9 -60,-59 -32,47 -83,-71 -36,83 27,91 -22,17 60,59zm1248 6583c63,-42 37,35 61,-54 36,-132 -10,-66 -27,-91 -56,21 -39,-53 -66,42 -50,177 2,67 32,103zm-3412 -2551c58,-28 78,64 72,-75 -4,-112 -22,-47 -68,-70 0,1 -117,-7 -4,145zm-290 -1225c21,58 -53,43 42,70 192,56 47,8 103,-36 -35,-66 28,-44 -52,-65 -126,-33 -82,23 -93,31zm-5356 -3095l-18 46c15,128 58,97 106,82 112,-34 73,-59 86,-77 -19,-13 -52,-43 -84,-53l-90 2zm8115 7257c60,-74 70,111 51,-115 -8,-93 -53,-79 -74,-92 -6,7 -48,-10 -51,93 -7,206 -25,60 74,114zm-358 -3417c71,-45 53,62 74,-83 25,-183 -18,-80 -39,-116 -80,37 -60,-87 -83,79 -26,191 22,83 48,120zm-17157 14c84,70 -122,41 42,30 72,-5 22,6 65,-27 -62,-52 99,-38 -41,-29 -76,5 -12,-10 -66,26zm5328 -1528c64,-38 48,39 53,-51 7,-103 14,-24 -49,-60 -33,49 -32,66 -4,111zm-27615 8986c78,-37 55,63 54,-88 -92,2 -45,-22 -73,27 -47,84 8,41 19,61zm32764 3684c-10,-69 1,-14 -45,-37 -107,108 -16,75 26,101 17,-36 27,-9 19,-64zm-6438 -13319c1,0 47,9 52,-34 12,-113 25,-18 -49,-67 -15,24 -88,9 -3,101zm-2549 -1315c-17,8 -170,44 -28,87 58,17 34,0 42,-20 45,-117 9,-15 -14,-67zm4500 3122c50,-16 41,47 55,-34 13,-73 3,-24 -22,-71 -54,37 -107,11 -33,105zm100 11390c59,41 -62,18 67,32 141,16 7,10 72,-28 -141,-125 -131,-11 -139,-4zm5906 333c26,-38 129,23 33,-116 -108,38 -98,26 -33,116zm-36586 -127c129,-18 2,10 67,-35 -65,-124 -157,-104 -185,26 57,9 59,18 118,9zm29127 1316c73,-30 40,-35 106,4 -33,22 14,19 -51,22 -116,5 -14,2 -55,-26zm-38 58l179 5 2 -117 -181 -1 0 113zm-23361 -15949c1,-1 61,10 45,-97 -16,-103 8,-32 -68,-78 -13,20 -65,-31 -41,104 21,124 2,35 64,71zm7079 11660c-101,8 -43,-8 -107,52 -56,54 -46,20 -62,107l176 18 -7 -177zm27110 -785c-83,-43 -52,-52 -103,26 49,34 -62,39 59,24 76,-9 15,3 44,-50zm-1211 -2487c86,-45 122,22 33,-115 -52,20 -45,-52 -59,40 -13,86 -25,15 26,75zm849 -5816c135,-10 77,32 107,-104 -166,22 -89,-39 -107,104zm-8045 -1393l59 -2c61,-97 37,-142 -53,-191 -5,7 -123,39 -6,193zm-12585 2344c3,-126 9,-31 -32,-70 -25,25 -18,13 -37,64l39 57 30 -51zm21445 8401c-173,28 -5,162 -5,162 17,-23 34,53 35,-88 2,-133 2,-16 -30,-74zm-7726 -10714c54,-16 48,46 60,-34 14,-86 9,-15 -27,-73 -49,31 -121,-28 -33,107zm2226 -437c29,-40 52,53 35,-69 -15,-105 -2,-34 -59,-73 -14,17 -58,-43 -43,72 17,126 -14,33 67,70zm-13381 14473c-108,-44 -32,2 -69,25 153,144 93,45 120,32 -37,-65 17,-30 -51,-57zm-16107 -2399c103,-46 70,-13 61,-117 -96,10 -54,-27 -90,34 -46,78 29,83 29,83zm6804 -10029c-8,-5 -18,-24 -24,-15l-95 -9c0,144 -25,145 119,124l0 -100c-90,37 -68,-13 -76,69 108,-5 13,38 76,-69zm-11048 7103c11,-112 26,21 -39,-41l8 93c60,-55 22,37 31,-52zm6 -5040c76,71 -99,41 38,30 70,-5 11,13 56,-27 -53,-44 90,-32 -36,-25 -65,4 -11,-8 -58,22zm701 2991c38,31 32,40 85,3 -102,-100 -85,-3 -85,-3zm38576 5668c152,93 87,13 102,3 -42,-43 17,-22 -57,-32l-45 29z"/>
|
||||
<path class="fil2" d="M22443 33531c91,-46 99,-37 3,-91 -65,80 -19,59 -3,91zm-9179 174l399 12 94 3 23 -102c121,-33 40,27 77,-81l-104 -24c-77,-2 -129,-55 -130,-59 -3,-10 -12,-11 -18,-15l-68 -4c-94,14 -152,38 -213,70l-167 11 -9 121 13 64 103 4zm3362 -16370c31,57 -35,35 43,50 123,23 4,11 64,-17 -27,-65 39,-44 -41,-57 -132,-21 -7,-12 -66,24zm10290 3584c-14,-80 9,-36 -56,-77 -3,4 -94,6 -19,84 51,53 74,-6 75,-7zm2277 11808c100,-46 108,-71 3,-122 -64,81 -39,56 -3,122zm-598 -13384c72,-96 69,-72 -26,-113 -75,90 -73,70 26,113zm-487 7658c-46,-62 -80,7 -82,7 65,99 -67,22 63,80 4,-5 100,22 19,-87zm-14360 -9918c34,-131 44,-88 -39,-138 -4,7 -53,-26 -32,67 28,121 -23,26 71,71zm13953 15010c6,-87 -6,-35 -58,-63 -75,84 -46,57 -4,130 44,-23 53,54 62,-67zm1319 -15162c106,-114 26,-92 4,-127 -51,29 -67,-45 -61,71 4,73 20,40 57,56zm2647 15024c-2,-2 8,-68 -88,-57 -109,12 -38,7 -73,64 105,50 64,60 161,-7zm-293 -3726c49,-51 48,-33 50,-171 -85,-2 -93,-38 -195,78 -83,12 -33,-18 -93,47l116 91c47,-29 48,-34 122,-45zm-2986 3302c-112,22 -39,12 -64,57 182,135 183,-23 184,-23 -10,-7 18,-61 -120,-34zm-1299 1033c-77,-44 -27,-43 -194,-4 19,157 34,164 112,169 166,12 14,18 78,-36l4 -129zm-29 818c-14,-28 44,-120 -225,-46 34,72 -28,65 93,92 164,37 124,-39 132,-46zm-7886 -16516l181 4c22,-158 -15,-177 -160,-176l-21 172zm1555 1675l149 114c98,86 124,57 174,-16 -50,-145 -13,-36 -146,-105 -44,-243 65,-253 -76,-325 -82,93 -26,15 -43,187 -15,145 -11,7 -58,145zm-16716 6826c57,-32 46,42 53,-46 6,-83 4,-30 -50,-59l-24 38c3,61 -12,24 21,67zm-1337 -2651c26,-6 45,9 64,-22 76,-123 9,-42 -9,-80 -79,40 -46,5 -55,102zm-4578 -6954l-112 -3 26 95c100,-38 42,50 86,-92zm7494 3833c33,-24 94,21 33,-122 -79,33 -50,-52 -63,48 -20,150 -15,19 30,74zm14390 12465c-111,-30 -19,-3 -63,20 25,35 -17,155 117,33 -23,-35 -5,-39 -54,-53zm-16337 1107c45,-72 -18,-62 -21,-68 -108,41 -55,-28 -72,101 119,-4 51,36 93,-33zm-15554 -8727c0,0 54,27 19,-104 -42,1 -53,-32 -84,34 -35,76 50,66 65,70zm29578 -7130c115,91 90,22 123,-26 -55,-41 62,-61 -72,-36 -85,16 -31,37 -51,62zm-15614 10133c9,-12 164,46 4,-107 -159,125 -4,106 -4,107zm19830 6238c-114,-3 -59,-5 -148,25 75,104 35,82 145,96l3 -121zm-1810 -750c-9,127 22,73 56,93 12,-18 98,-37 5,-177 -39,23 -54,-4 -61,84zm-22909 -1701c109,-48 103,-21 88,-170l-170 17 -8 103 28 61 62 -11zm17260 -7459c57,-31 46,52 67,-70 25,-145 -11,-61 -33,-93 -63,23 -49,-63 -69,61 -24,151 20,79 35,102zm6292 8218c-87,10 -54,14 -87,55 20,17 25,52 112,48 129,-7 69,-26 89,-70 -34,-24 -5,-45 -114,-33zm-18726 -12582l138 126c48,-78 33,-47 37,-155 -115,-28 -88,-44 -175,29zm5425 2089c111,8 43,39 121,-26l69 -111c-57,-37 50,-12 -63,-36 -108,-23 -9,-17 -111,3l-16 170zm198 -5155c69,-15 103,32 92,-91 -10,-106 -55,-86 -92,-103 -90,200 -13,173 0,194zm-4497 9344c43,-116 -3,-63 -8,-78 -188,-98 -155,47 -70,159 12,-9 44,9 78,-81zm-14595 -8298l-38 -84 -224 -26c12,84 -6,39 36,90 11,13 12,8 29,28 102,110 -28,62 101,78 134,18 76,-16 96,-27l0 -59zm16516 14224c-52,-40 -1,-28 -113,-50 -42,-8 -243,-19 -291,67 61,129 109,18 238,86 55,29 -10,50 104,69 197,32 132,-42 145,-52 -48,-181 24,-57 -88,-111l5 -9zm-15098 58c48,-69 45,-72 17,-145 -274,51 -23,42 -250,167 30,-165 -35,-99 -48,-113l-11 227 209 8 83 -144zm10508 -4775c27,-191 50,60 -34,-54 -79,69 -75,58 -4,123 52,-55 26,22 38,-69zm2333 -7579c33,-5 126,10 57,-109 -99,52 -45,-60 -57,109zm-3429 -2611c31,53 -29,36 45,50 112,21 8,9 62,-17 -23,-52 62,-39 -41,-56 -76,-13 -10,-11 -66,23zm-5194 1548c20,-171 28,-57 -49,-102 -23,40 -71,-39 -24,67 24,53 -2,25 73,35zm2621 14176c16,-129 6,-16 -31,-67 -97,48 -106,68 -4,128 34,-41 21,52 35,-61zm20632 -662c16,-119 -2,-25 -34,-66 -97,47 -111,67 -4,122 35,-34 23,48 38,-56zm-26456 -15122c101,69 77,57 133,-25 -31,-22 47,-55 -73,-37 -124,19 -29,27 -60,62zm-3048 11843c12,19 -42,82 104,51 57,-12 47,-31 63,-80 -76,-39 -83,-41 -168,-21l1 50zm591 1476c-69,86 -68,86 -5,161 100,-47 96,-110 5,-161zm9675 2249c133,-45 121,-5 97,-144 -185,-45 -50,-16 -91,31 11,22 3,32 23,36 48,12 16,9 52,-9 -40,-40 -1,-18 -75,-27 -8,6 -43,27 -44,28 -15,11 -33,39 -36,33 -3,-7 -13,13 -19,19l7 24c-84,-5 -18,-34 -119,21l-151 150c-8,14 -26,96 -31,118 184,-1 48,-2 196,-98 58,-38 107,-51 184,-77l16 -87 -9 -18zm-10948 -16354c55,-82 67,-123 -24,-178 -4,5 -50,-31 -46,89 4,132 36,74 70,89zm5381 2060c95,56 114,57 192,-23 -74,-75 -188,-91 -192,23zm4150 14183c1,-1 63,-22 50,-106 -19,-117 0,-36 -74,-72 -29,39 -120,103 24,178zm17482 -2004c-59,-80 -103,-72 -188,-35 18,24 -69,68 85,81 140,12 94,-39 103,-46zm-13680 -1675c142,42 170,46 247,-29 -36,-39 -44,-68 -144,-58 -99,9 -73,21 -103,87zm-8064 1221l-36 23c-90,81 -54,49 -55,120 261,25 188,-170 188,-171 -115,-6 33,-13 -60,2l-37 26c8,17 -2,42 39,48 4,1 17,-4 20,-4 4,0 15,-7 22,-10 -39,-35 7,-19 -81,-34zm8634 -1952c-7,-10 54,-79 -138,-26 10,87 13,131 91,196 33,-28 19,-1 54,-52 25,-33 -21,88 23,-39 49,-141 5,-71 -30,-79l-79 -1 26 81 53 -80zm-7088 2769c-11,-74 -89,-139 -148,-163l-44 119 162 129c15,-21 49,45 30,-85zm-7692 303l-2 61 260 28c12,-216 -160,-128 -258,-89zm39584 -10253c14,28 -26,64 64,64 136,1 28,20 79,-60 -44,-33 10,-44 -75,-38 -129,7 -19,-3 -68,34zm419 750c28,-36 34,52 41,-69 10,-164 -9,-60 -36,-101 -30,17 -56,-27 -71,73 -20,138 30,78 66,97zm3065 2473c56,-72 68,-95 66,-198 -91,-19 -135,-38 -154,50 -19,87 88,148 88,148zm2397 -3620c57,42 -53,30 105,50 159,19 21,7 114,-15 -23,-95 27,-126 -112,-132 -103,-5 -87,13 -107,97zm-719 4372l-51 -56c213,-116 93,-238 -69,-239 -152,-1 -176,38 -165,195l14 39c14,128 95,141 226,140l45 -79zm-11881 -7680c95,59 33,27 75,3l-58 -18c-2,-8 -11,11 -17,15zm-21198 15445c35,43 20,47 60,29 5,-2 14,-15 21,-22l-81 -7zm5007 1132l96 3 -5 -58c-40,-60 5,-36 -76,-65 -36,-12 -55,0 -88,1l4 117 69 2zm19325 -14421l-61 0c-35,54 -27,-70 -32,67 -4,136 12,61 31,94 73,-20 31,13 65,-51 145,-35 46,16 95,-68 -56,-55 16,-32 -98,-42zm-8429 13335c40,-6 207,-169 -164,-173 -69,95 -80,76 -101,170l265 3zm14572 -9261c47,74 56,123 127,4 -117,-124 -105,-20 -127,-4zm909 -1194c54,84 169,131 199,-23 -27,-13 -10,-39 -116,-25 -56,7 -60,9 -83,48zm271 2163c-1,-1 33,-56 -85,-37 -108,17 -29,-7 -85,61 38,68 -51,70 108,81 68,5 70,-17 106,-46 -12,-97 2,-33 -44,-59l-117 16 83 62c42,-51 19,-10 34,-78zm2226 -30c105,20 81,29 165,-11 -16,-86 -60,-131 -144,-155 -47,81 -42,60 -21,166zm-680 -1938c37,-37 13,14 35,-54 59,-183 70,-195 20,-219 -8,-1 -19,-13 -23,-4 -2,7 -74,-30 -94,109 -15,102 -7,135 62,168zm-7886 -1031c174,9 174,-15 198,-181l-195 0 -3 181zm-435 -3677c1,-132 -96,-207 -218,-204 -14,163 61,224 218,204zm9068 10670l-229 -20c16,143 33,200 192,182 19,-35 15,-15 28,-68 1,-1 7,-80 9,-94zm-11458 -8654l179 16c42,-78 53,-79 68,-188l-240 -2 -7 174zm-13484 -1787c67,65 -101,40 38,31 69,-5 24,3 66,-28 -78,-63 97,-40 -43,-30 -88,6 4,-25 -61,27zm15318 6420c8,8 -38,96 94,3 -76,-59 111,-37 -40,-27 -71,5 -10,-11 -54,24zm-4361 -5494c0,-176 44,-57 -80,-98 19,110 -24,65 80,98zm5776 11913c-52,-67 -17,-71 -103,-32 75,108 -101,-50 18,46 3,3 12,10 15,12 123,59 36,-5 70,-26zm452 -9192c-15,-105 18,-46 -63,-89 -52,58 -31,28 -37,112l0 24c138,141 57,117 171,135l18 -141 -89 -41zm1300 -184c26,36 -55,67 177,58 3,-16 69,-189 -108,-115 -67,28 -30,-2 -69,57zm-6644 5321c34,-145 -23,-89 -32,-106 -123,17 -85,-60 -108,72 -22,127 47,87 73,100 61,-30 38,56 67,-66zm12539 -5533c161,1 121,36 140,-122 -54,-35 -31,-46 -161,-30 -7,73 -19,77 21,152zm-7899 4642l158 -3 14 -151 -161 -4 -11 158zm-17041 -7038c56,111 45,115 103,4 -51,-38 -52,-32 -103,-4zm9438 14921c155,-74 35,-103 33,-108 -148,63 -33,108 -33,108zm-15491 -14617c76,-131 38,-84 -75,-106 30,146 -22,74 75,106zm2595 5412c114,-55 112,-64 4,-122 -17,20 -39,-42 -33,60 3,62 0,30 29,62zm3715 -5475c14,118 -32,80 93,86 37,-95 5,-127 -93,-86zm-11753 7101c5,-101 4,-86 -99,-104 -13,126 -1,117 99,104zm5801 -8233c54,-27 68,45 62,-68 -4,-75 -27,-45 -58,-61 -3,3 -85,14 -4,129zm-10835 7881c-39,-69 -44,-121 -135,-4 59,52 -68,23 66,40 114,15 28,-3 69,-36zm25397 -740c-39,10 -173,13 -98,95 96,105 85,-47 98,-95zm-2065 8351c-44,-72 23,-10 -75,-89 -65,-51 0,-9 -75,-42l-43 107 31 161c93,9 88,6 142,-32l20 -105zm-14954 -14214c87,52 55,48 150,6 -45,-139 -121,-139 -150,-6zm2674 8593c-160,1 -84,46 -93,80l164 16c3,-66 28,-97 -71,-96zm-12857 -11652c59,116 145,73 198,6 -89,-66 -119,-55 -198,-6zm14729 9603c9,31 -34,70 86,81 110,11 71,-13 85,-75 -84,-44 -68,-38 -171,-6zm-1945 -4959c-113,-51 -59,-57 -140,3l-3 59c93,63 166,72 228,-22 -42,-48 23,-26 -85,-40l-115 0c37,83 -28,35 82,68 55,-38 15,12 33,-68zm1792 10456c-77,-56 -141,-72 -205,23 16,12 15,56 118,47 109,-8 68,-30 87,-70zm6791 -1117c36,-46 36,70 46,-94 7,-128 -25,-68 -41,-87 -41,17 -64,-27 -70,84 -8,141 28,77 65,97zm-19845 -1831c156,4 126,-56 115,-191 -148,47 -119,6 -115,191zm28588 2560c110,14 138,-57 145,-166l-129 10c-85,158 -17,155 -16,156zm-24536 1960c-95,25 -67,45 -79,133 122,4 100,21 190,-35 -16,-82 22,-133 -111,-98zm-1339 -12555c34,-29 35,29 35,-82 -176,-85 -220,125 -198,245 232,90 139,21 169,-18l-8 -56 2 -89zm-138 10191c6,-3 15,-9 19,-11 187,-98 143,-30 160,-133 -81,-49 -97,-61 -235,49 -9,7 -15,11 -19,14l-10 -4 4 8c-6,4 -7,6 -36,41 47,43 -13,15 61,44 105,40 38,-2 56,-8zm8367 -12746l142 -3 16 -140c-106,-42 -55,-43 -174,-39 -38,108 -18,70 16,182zm13540 9864c20,-155 32,-118 -57,-201l-114 -69 -18 135c4,4 38,33 46,42 112,117 -30,57 143,93zm988 4753c45,-102 30,10 24,-135l-34 -127c-82,44 -11,4 -56,41l-47 40c-58,65 -44,-8 -43,128 63,55 64,54 156,53zm12226 -12623c69,-36 48,37 60,-51 10,-77 0,-29 -27,-74 -74,34 -47,-58 -62,47 -11,85 -16,23 29,78zm-541 532c79,-38 56,43 72,-73 18,-134 -33,-95 -37,-102 -62,28 -51,-64 -72,68 -25,164 18,78 37,107zm3249 -2408c42,-47 39,67 47,-70 10,-158 -17,-67 -42,-99 -63,25 -63,-60 -82,69 -19,138 47,86 77,100zm1303 9111c11,10 -39,54 99,53 165,-1 78,-27 103,-76 -9,-7 34,-50 -103,-50 -165,1 -71,41 -99,73zm-492 -3647c92,-9 106,-16 198,-88 -55,-161 -128,-160 -198,-171 8,167 44,104 0,259zm-24691 -3316c88,44 53,43 105,-26 -39,-27 54,-78 -66,-29 -4,1 -13,8 -16,10l-23 45zm13670 13200c-33,-129 32,-90 -103,-104 13,123 -21,66 103,104zm-2530 -391c-27,-47 15,-109 -119,-33 20,31 -50,44 47,68 133,33 20,3 72,-35zm7333 519c-75,-6 -55,36 -70,58 59,45 -67,20 67,36 128,15 17,5 68,-31 -26,-54 53,-54 -65,-63zm-2346 -1174c-21,-102 14,-11 -60,-51 -44,43 -78,-66 -39,94 19,78 18,22 64,42 41,-48 52,-4 35,-85zm-2671 1531c-126,3 -69,18 -100,67 33,26 41,98 222,6 -23,-40 -11,-75 -122,-73zm2389 -1391c-144,-10 -216,11 -140,175l167 10 -27 -185zm-13510 -6878c31,-55 21,-40 2,-86 -55,59 -65,-88 -31,42 34,130 -97,-14 29,44zm11036 7899c51,38 -7,20 62,28 135,17 -26,24 42,-24 -37,-35 3,-24 -62,-33 -162,-23 46,-43 -42,29zm-26597 -10879c61,73 10,58 96,32 -17,-44 72,-25 -24,-45 -72,-14 -22,-8 -72,13zm24876 10019c-12,-82 2,-14 -47,-36 -75,86 -25,63 27,92 24,-39 30,7 20,-56zm-13300 -13607c15,-24 58,33 23,-60 -25,-65 14,-8 -49,-43 -39,74 -64,63 26,103zm-1474 10770c72,-89 63,-70 -26,-109 -102,108 -21,86 26,109zm2490 -9768c5,-7 104,11 5,-148 -45,51 -53,-76 -44,73 5,81 3,34 39,75zm-455 582l45 -26c-21,-109 31,-69 -102,-93 -6,111 -39,73 57,119zm-2646 4875c5,-126 -2,-96 -98,-104 -10,117 -38,124 98,104zm-15387 -7335c19,35 -38,43 63,61 137,25 73,-13 97,-27 -4,-10 32,-155 -160,-34zm3776 14905c-5,113 33,83 36,87 97,-48 94,-123 6,-180 -22,28 -35,-54 -42,93zm16124 -7293c1,0 19,130 175,5 -14,-31 11,-58 -77,-63 -125,-7 -71,37 -98,58zm4168 4706c37,-17 60,39 74,-88 13,-125 -31,-79 -39,-89 -47,21 -58,-27 -69,83 -13,129 25,80 34,94zm-14149 1449c-77,-12 -83,28 -88,128l196 -3c-13,-79 -18,-110 -108,-125zm-10285 -377c145,13 156,25 271,-27 -60,-66 -141,-122 -250,-121l-21 148zm30878 -12750c85,40 49,48 105,-26 -21,-16 24,-117 -105,26zm-4545 -547c57,-82 74,-71 -26,-119 -64,93 -65,76 26,119zm6389 36c29,-122 43,-90 -83,-116 14,175 -39,72 83,116zm5155 2988c-2,44 -19,42 29,111 108,-35 40,70 81,-93 0,-4 -13,-87 -13,-87 -15,-49 -13,-36 -31,-65 -82,38 -49,-29 -66,134zm-7078 -530c164,-26 121,59 106,-100 -107,-4 -71,-31 -106,100zm1288 -911c5,-125 48,-86 -66,-119 -93,100 -38,96 66,119zm2390 12606c-127,16 -58,-23 -117,61 77,102 120,95 117,-61zm-6680 -14484c93,-49 102,-26 33,-126 -50,15 -40,-45 -66,34 -44,140 5,62 33,92zm-6631 -463c15,26 -10,57 69,75 138,31 72,-16 95,-31 -35,-82 47,-48 -66,-68 -28,-5 -93,19 -98,24zm9161 3323c39,-11 65,56 83,-82 13,-103 -26,-89 -48,-112 -98,44 -81,122 -35,194zm11122 7323c77,-41 52,57 68,-98 5,-49 -11,-64 -20,-96 -60,21 -77,-45 -93,90 -16,134 36,91 45,104zm-6944 6433l64 2c34,-109 52,-107 -11,-201 -30,8 -66,-56 -83,91 -7,64 4,48 30,108zm-9852 -12707c33,-80 15,-192 -49,-252 -81,-76 -14,-26 -117,-47 50,222 25,40 89,163 45,86 -28,59 77,136zm17007 11129c-5,-4 -12,-10 -17,-14l-83 -58c-95,106 -8,225 151,201 10,-144 18,-76 -51,-129zm-14522 -7357l6 118 177 17 4 -157c-119,-17 -94,-25 -187,22zm1633 621c122,-9 53,43 137,-39 83,-80 31,-20 54,-141l-180 10 -11 170zm4070 -5651l169 46c4,-99 16,-85 -35,-146 -85,-102 -17,-49 -154,-73l20 173zm10175 12053c-9,-85 14,-6 -49,-52 -37,41 -36,-9 -29,69 9,97 -12,14 55,43 36,-56 37,58 23,-60zm-8682 -13171c38,60 -39,38 51,53 70,12 25,3 66,-21 -12,-32 50,-133 -117,-32zm2108 1273c32,-19 113,33 33,-122 -78,35 -51,-47 -63,49 -17,128 -11,22 30,73zm-2180 -3663l-109 -3c3,174 -2,49 43,106 98,-52 42,59 66,-103zm10237 6624c21,-31 58,42 35,-72 -21,-101 16,-22 -69,-66 -51,93 -65,71 34,138zm-4681 8278c35,-113 -29,-91 -30,-94 -79,35 -36,-30 -61,65 -8,30 9,72 15,95 49,-25 47,25 76,-66zm1712 -10475c90,-11 72,27 105,-40 54,-109 -36,-81 -61,-94l-37 44c-16,57 -18,8 -7,90zm-2159 653c14,19 -15,66 80,65 117,-2 32,5 75,-60 -19,-16 33,-129 -155,-5zm-9160 -4516c140,63 103,33 149,-46 -10,-8 40,-74 -77,-47 -186,44 -11,10 -72,93zm14791 10390c-149,-15 -74,13 -105,64 54,49 -82,47 102,53 102,4 97,-46 99,-47 -19,-29 7,-60 -96,-70zm-7396 -6688c36,71 -84,46 66,62 8,-7 18,-27 22,-21l74 -89c49,-90 34,-58 -1,-120 -66,20 -27,3 -73,42 -5,4 -10,10 -14,14 -5,4 -11,10 -15,14 -4,5 -11,12 -16,16l-35 34 -8 48zm3324 -2979c19,41 -49,73 95,76 129,2 70,-21 95,-70 -7,-6 38,-52 -90,-53 -165,-1 -56,12 -100,47zm-4325 1161l115 0c20,-126 28,-84 -30,-190 -117,49 -118,87 -85,190zm5264 1728c2,-3 48,17 50,-92 4,-170 -3,-48 -44,-101 -39,16 -77,-51 -90,107 -6,77 17,64 84,86zm3736 8908c128,-174 17,-185 7,-200 -65,16 -98,-21 -96,99 2,114 50,86 89,101zm-577 -7651c-69,-84 28,-31 -83,-63 -48,-14 -90,-4 -138,1 13,162 117,193 221,62zm-10793 -2729c100,-7 113,56 202,-184l-174 3 -28 181zm11216 4612c69,-72 58,-54 46,-178l-199 -7c-4,7 -10,9 -12,18l-76 95c135,22 68,-7 161,87l80 -15zm-25273 1701c76,60 6,56 86,3 -47,-47 102,-40 -34,-29 -72,6 -2,-17 -52,26zm-2321 -4177c9,14 14,125 94,3 -55,-40 95,-28 -36,-22 -55,3 -22,-1 -58,19zm3532 6166c27,41 -52,47 54,45 61,-2 15,-6 40,-42 -34,-25 -1,-17 -55,-23 -4,0 -17,14 -19,7 -3,-7 -13,9 -20,13zm9741 -9188c141,-29 65,23 97,-75 -192,-10 -27,11 -97,75zm-816 7909c15,-101 13,5 -29,-35 -126,99 -5,94 -3,96 46,-55 21,17 32,-61zm4719 4713c-57,112 -6,24 19,71 20,-12 147,-53 24,-91 -71,-21 -31,-5 -43,20zm-8123 -431c-15,-86 16,-10 -58,-43 -82,124 13,93 35,107 35,-58 46,63 23,-64zm17490 -3007c175,100 121,20 148,5 -164,-141 -131,-17 -148,-5zm-22342 4930c132,-4 153,38 190,-77l-182 -33c-126,37 -57,-3 -107,63 47,51 -83,44 99,47zm12980 -12068c-5,14 -15,15 -17,31 -2,16 -11,19 -13,27 -5,31 -5,60 -3,92l176 14 5 -153 -148 -11zm3361 -432c36,-54 25,-37 3,-87 -60,72 -60,-95 -28,43 33,144 -77,-37 25,44zm-4106 70c11,-16 62,31 29,-65 -28,-79 21,-7 -55,-51 -27,44 -74,-63 -27,70 23,65 -11,17 53,46zm4107 -1014c83,-42 48,57 62,-54 19,-151 12,-10 -29,-78 -91,38 -53,-66 -66,49 -13,104 -23,19 33,83zm5309 -1291c25,-37 79,60 36,-83 -31,-103 26,-8 -61,-59 -32,47 -82,-71 -36,83 28,92 -21,17 61,59zm1248 6583c63,-42 37,35 61,-54 35,-132 -11,-66 -27,-91 -56,21 -40,-53 -67,42 -50,177 3,67 33,103zm-3413 -2551c58,-28 79,64 73,-75 -5,-112 -22,-47 -68,-70 -1,1 -117,-7 -5,145zm-289 -1224c21,57 -53,42 41,69 192,56 47,8 104,-36 -35,-66 27,-44 -52,-65 -126,-33 -82,23 -93,32zm-5357 -3096l-17 46c15,129 58,97 106,82 112,-34 72,-59 86,-77 -20,-13 -52,-43 -84,-53l-91 2zm8116 7257c60,-74 70,111 51,-114 -8,-94 -53,-80 -74,-93 -6,8 -48,-10 -52,93 -7,206 -24,60 75,114zm-359 -3417c72,-45 54,62 74,-83 26,-183 -17,-80 -38,-116 -80,37 -61,-87 -83,79 -26,191 21,83 47,120zm-17157 14c85,70 -121,41 43,30 71,-4 21,6 65,-27 -62,-52 98,-38 -42,-29 -76,5 -12,-10 -66,26zm5329 -1528c63,-38 47,39 53,-51 6,-102 14,-24 -50,-60 -32,49 -31,66 -3,111zm-27615 8986c78,-36 55,63 54,-88 -92,3 -46,-22 -73,28 -47,83 8,40 19,60zm32764 3684c-10,-69 0,-14 -45,-36 -107,107 -16,74 26,100 17,-35 26,-9 19,-64zm-6439 -13319c1,0 48,9 52,-33 12,-114 25,-19 -48,-68 -16,24 -89,9 -4,101zm-2549 -1315c-16,8 -169,44 -28,87 59,17 35,0 43,-20 45,-117 8,-15 -15,-67zm4501 3122c50,-16 41,47 55,-34 13,-73 3,-24 -23,-71 -53,37 -106,11 -32,105zm100 11390c59,41 -62,18 66,32 141,16 8,10 73,-28 -142,-125 -131,-11 -139,-4zm5906 333c25,-38 129,23 33,-116 -109,38 -98,26 -33,116zm-36586 -127c129,-18 2,10 67,-34 -65,-125 -157,-104 -185,25 57,9 59,18 118,9zm29127 1317c73,-31 40,-35 105,3 -33,22 15,19 -51,22 -115,5 -13,3 -54,-25zm-38 57l179 5 2 -117 -181 -1 0 113zm-23361 -15949c1,-1 61,10 45,-96 -16,-104 8,-33 -69,-78 -13,19 -64,-32 -40,103 21,124 1,35 64,71zm7078 11660c-100,8 -43,-8 -106,52 -56,54 -46,20 -62,107l175 18 -7 -177zm27110 -785c-82,-43 -51,-52 -102,26 49,34 -62,39 58,24 76,-9 15,3 44,-50zm-1210 -2487c86,-45 122,22 32,-114 -51,19 -45,-53 -58,39 -14,86 -25,15 26,75zm849 -5816c135,-10 76,32 106,-104 -166,22 -89,-39 -106,104zm-8045 -1393l58 -2c62,-97 37,-142 -52,-191 -5,7 -124,39 -6,193zm-12585 2344c3,-126 9,-31 -32,-70 -26,25 -18,13 -37,64l38 57 31 -51zm21445 8402c-173,27 -5,161 -5,161 16,-23 33,53 35,-88 2,-133 2,-16 -30,-73zm-7726 -10715c54,-16 48,46 60,-34 13,-86 8,-15 -27,-73 -49,31 -122,-28 -33,107zm2226 -437c29,-40 52,53 35,-69 -16,-105 -3,-34 -60,-73 -13,17 -57,-43 -42,72 16,126 -15,33 67,70zm-13381 14473c-109,-44 -32,2 -69,25 153,144 93,45 120,33 -37,-65 17,-30 -51,-58zm-16107 -2399c103,-46 70,-13 61,-117 -97,10 -54,-27 -90,34 -46,78 29,83 29,83zm6804 -10029c-8,-5 -18,-24 -24,-15l-96 -9c1,144 -24,145 120,124l0 -100c-90,37 -68,-13 -76,69 108,-5 13,38 76,-69zm-11049 7103c12,-112 26,21 -38,-41l7 93c60,-55 22,37 31,-52zm7 -5040c76,71 -99,41 37,30 71,-5 11,13 56,-27 -52,-44 90,-32 -35,-25 -65,4 -11,-8 -58,22zm700 2992c39,30 32,39 86,2 -102,-100 -86,-3 -86,-2zm38577 5667c152,93 87,13 102,3 -42,-43 16,-22 -58,-32l-44 29z"/>
|
||||
</g>
|
||||
</g>
|
||||
<path class="fil3" d="M20432 0c8718,-38 15581,2266 15458,9209 -25,787 -197,745 -1206,434 -5376,-1662 -8578,-3016 -12410,-6248 -1067,-900 -1945,-1797 -2743,-2837 -478,-648 -8,-545 901,-558zm-18416 19428c1785,-4186 2803,-6725 6956,-9214 1814,-1088 3728,-1795 5744,-2401 5011,-1469 9917,-953 14495,540 1697,562 2833,1208 4123,2158 602,450 989,178 1872,-71 402,-113 1572,-529 1912,-305 933,615 -675,1120 -1291,1389 -1798,785 -1256,251 -1265,1780 -43,7030 -7302,11174 -13250,12374 -3409,688 -7029,1146 -9343,1499 -5916,900 -5060,-1185 -9071,-2819 -937,-381 -2531,-341 -2879,-1361 -223,-655 1540,-2751 1997,-3569z"/>
|
||||
<path class="fil4" d="M10113 32164l548 -85c566,-87 655,-815 655,-1696 0,-1108 -152,-1559 -630,-1485l-573 89 0 3177zm-872 -3711l1541 -239c770,-120 1425,167 1425,1818l0 225c0,1013 -203,2283 -1374,2464l-1592 246 0 -4514z"/>
|
||||
<path class="fil4" d="M14753 30269l-349 -2047 -421 2167 770 -120zm-922 -2527l1171 -182 1019 4358 -943 146 -216 -1141 -987 152 -274 1218 -935 145 1165 -4696z"/>
|
||||
<polygon class="fil4" points="17968,31617 17096,31752 17096,27924 16014,28092 16014,27404 19050,26934 19050,27622 17968,27790 "/>
|
||||
<path class="fil5" d="M24269 30700l88 -3885 -824 115 -106 4640 2357 -329 17 -755 -1532 214zm2552 396l105 -4640 -824 115 -106 4640 825 -115zm3488 -5112l-525 1811c-157,575 -298,1154 -410,1687l-17 3c-83,-518 -193,-1044 -319,-1586l-423 -1679 -1020 143 -395 4680 752 -105 124 -1798c41,-577 85,-1269 109,-1855l17 -3c87,553 207,1162 334,1668l432 1780 624 -87 566 -1950c157,-539 327,-1176 462,-1748l17 -3c-16,472 -6,1216 3,1822l37 1793 785 -109 -139 -4606 -1014 142zm2380 3547l29 -1291 1399 -196 16 -730 -1398 196 25 -1123 1483 -207 17 -748 -2307 322 -106 4640 2396 -334 17 -749 -1571 220z"/>
|
||||
<path class="fil4" d="M21036 29298l-350 -2048 -420 2166 770 -118zm-923 -2529l1172 -182 1019 4359 -943 145 -217 -1141 -986 152 -274 1218 -936 145 1165 -4696z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 69 B |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 174 B |
|
Before Width: | Height: | Size: 173 B |
|
Before Width: | Height: | Size: 173 B |
|
Before Width: | Height: | Size: 180 B |
|
Before Width: | Height: | Size: 179 B |
|
Before Width: | Height: | Size: 180 B |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 255 B |
|
Before Width: | Height: | Size: 8.9 KiB |
@@ -1,16 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#555555;}
|
||||
</style>
|
||||
<g>
|
||||
<path class="st0" d="M476.7,88.3h-88.3c0-48.8-39.5-88.3-88.3-88.3s-88.3,39.5-88.3,88.3h-88.3c-19.5,0-35.3,15.8-35.3,35.3v88.3
|
||||
C39.5,211.9,0,251.4,0,300.1s39.5,88.3,88.3,88.3v88.3c0,19.5,15.8,35.3,35.3,35.3h123.6v-35.3c0-29.3,23.7-53,53-53s53,23.7,53,53
|
||||
V512h123.6c19.5,0,35.3-15.8,35.3-35.3V353.1h-35.3c-29.3,0-53-23.7-53-53s23.7-53,53-53H512V123.6
|
||||
C512,104.1,496.2,88.3,476.7,88.3z M388.4,300.1c0,48.8,39.5,88.3,88.3,88.3v88.3h-88.3c0-31.5-16.8-60.7-44.1-76.5
|
||||
c-27.3-15.8-61-15.8-88.3,0c-27.3,15.8-44.1,44.9-44.1,76.5h-88.3V353.1H88.3c-29.3,0-53-23.7-53-53s23.7-53,53-53h35.3V123.6
|
||||
h123.6V88.3c0-29.3,23.7-53,53-53s53,23.7,53,53v35.3h123.6v88.3C427.9,211.9,388.4,251.4,388.4,300.1z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 141 B |
|
Before Width: | Height: | Size: 222 B |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1023 B |
|
Before Width: | Height: | Size: 889 B |
|
Before Width: | Height: | Size: 81 B |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 15 KiB |
@@ -1,16 +0,0 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" />
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
<path d="M10 12a1 1 0 0 0-1 1v1a1 1 0 0 1-1 1 1 1 0 0 1 1 1v1a1 1 0 0 0 1 1" />
|
||||
<path d="M14 18a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1 1 1 0 0 1-1-1v-1a1 1 0 0 0-1-1" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 498 B |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 431 B |
|
Before Width: | Height: | Size: 725 B |
|
Before Width: | Height: | Size: 996 B |
|
Before Width: | Height: | Size: 743 B |
|
Before Width: | Height: | Size: 972 B |
|
Before Width: | Height: | Size: 517 B |
|
Before Width: | Height: | Size: 665 B |
|
Before Width: | Height: | Size: 692 B |
|
Before Width: | Height: | Size: 271 B |
|
Before Width: | Height: | Size: 691 B |
|
Before Width: | Height: | Size: 71 B |
|
Before Width: | Height: | Size: 71 B |
|
Before Width: | Height: | Size: 537 B |
|
Before Width: | Height: | Size: 305 B |
|
Before Width: | Height: | Size: 350 B |
|
Before Width: | Height: | Size: 346 B |
|
Before Width: | Height: | Size: 1000 B |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 54 B |
|
Before Width: | Height: | Size: 54 B |