Files
app-autotest/site/api/auth.py
T
naeel 764592ba60 fix: код-ревью #5 #6 #8 #9 #10 — 5 исправлений (v1.2.43)
#5 api_history: try/finally для conn/cur (как везде)
#6 stand_name: точное сравнение URL вместо substring
#8 get_real_client: удалён мёртвый алиас
#9 UNIQUE индекс idx_runs_op_uid для предотвращения гонок
#10 flock flush: f.flush() перед LOCK_UN в ротации лога
#7 (отступ scenario.py) пропущен — только косметика, риск сломать else:
2026-08-04 08:02:08 +04:00

178 lines
7.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Единый модуль аутентификации и создания HTTP-клиента.
Раньше _client(), _client_id(), _stand(), _token_info() были продублированы
в main.py и api_test.py с идентичным или почти идентичным кодом.
Теперь всё здесь — один источник правды для всех роутов.
Режимы работы (2026-08-03):
- Эмуляция (mode=polygon): всё в полигон, стенд из селектора (dev/test/prod)
- Облако (mode=cloud): реальный API, стенд автоопределяется по токену
Cookie (устанавливаются main.py:action=set_mode):
- mode — "polygon" (по умолчанию) или "cloud"
- polygon_stand — "test" (по умолчанию), "dev", "prod"
Функции (все без аргументов — берут данные из Flask request/current_app):
get_mode() — "polygon" / "cloud"
get_polygon_stand() — "dev" / "test" / "prod"
get_token() — токен: cookie → env (в эмуляции только env)
get_client() — HttpClient с учётом режима и стенда
get_real_client() — алиас get_client() (историческая совместимость)
get_client_id() — ClientID из JWT (base64url, без проверки подписи)
get_stand() — "polygon_test" / "dev" / "test"
get_token_info() — {email, company, client_id} из JWT для UI
get_token_masked() — маскированный токен (abc...xyz) для placeholder
"""
import base64
import json
from flask import request, current_app, g
from api.http_client import HttpClient, detect_endpoint, stand_name, STANDS
def _cached_detect(token):
"""detect_endpoint с кешированием на время запроса (flask.g).
get_client() и get_stand() вызывают detect_endpoint() независимо —
это двойной HTTP-запрос к Nubes API. Кеш в g решает проблему."""
cache = g.get('_detected_endpoint')
if cache is None:
g._detected_endpoint = cache = {}
if token not in cache:
endpoint = current_app.config["NUBES_API_ENDPOINT"]
if endpoint in STANDS:
endpoint = detect_endpoint(token) or endpoint
cache[token] = endpoint
return cache[token]
def get_mode():
"""Режим: 'polygon' (эмуляция) или 'cloud' (реальное облако).
Если POLYGON_ENDPOINT не задан в конфиге — всегда облако (селектор не нужен).
Иначе — из cookie, по умолчанию эмуляция."""
if not current_app.config.get("POLYGON_ENDPOINT", ""):
return "cloud"
mode = request.cookies.get("mode", "polygon")
if mode not in ("polygon", "cloud"):
mode = "polygon"
return mode
def get_polygon_stand():
"""Стенд полигона: 'test' (по умолчанию), 'dev', 'prod'.
Используется ТОЛЬКО в режиме эмуляции — для построения URL и load_service_ids."""
stand = request.cookies.get("polygon_stand", "test")
if stand not in ("dev", "test", "prod"):
stand = "test"
return stand
def get_token():
"""Получить активный токен.
В эмуляции — ТОЛЬКО env-токен (пользовательский ввод дезактивирован).
В облаке — cookie → env (как раньше)."""
if get_mode() == "polygon":
return current_app.config["NUBES_API_TOKEN"]
return request.cookies.get("token") or current_app.config["NUBES_API_TOKEN"]
def get_client():
"""HttpClient с учётом режима и стенда.
Эмуляция: polygon_url + "/" + stand → https://polygon.../{stand}/api/v1/svc
Облако: реальный API с автоопределением стенда (dev/test по токену)."""
token = get_token()
mode = get_mode()
if mode == "polygon":
polygon = current_app.config.get("POLYGON_ENDPOINT", "")
stand = get_polygon_stand()
# POLYGON_ENDPOINT = "https://polygon.../api/v1/svc"
# Стенд ВСТАВЛЯЕТСЯ перед /api/v1/svc:
# "https://polygon.../api/v1/svc" → "https://polygon.../test/api/v1/svc"
# Простая замена "/api/v1/svc" → "/{stand}/api/v1/svc"
return HttpClient(polygon.replace("/api/v1/svc", f"/{stand}/api/v1/svc"), token)
# Облако: NUBES_API_ENDPOINT с автоопределением (кешированным)
return HttpClient(_cached_detect(token), token)
def get_client_id():
"""Извлечь ClientID из payload JWT-токена (base64url, без проверки подписи).
JWT состоит из трёх частей: header.payload.signature, разделённых точкой.
Нам нужен ТОЛЬКО payload — он в base64url (не base64!).
ClientID используется для:
- Изоляции данных в БД (runs.client_id)
- Изоляции трекера инстансов (/tmp/instances-{clientId}-{stand}.json)
- Отображения в UI
Безопасность: мы НЕ проверяем подпись — это не нужно.
Токен уже проверен Nubes API (detect_endpoint делает реальный запрос)."""
token = get_token()
try:
parts = token.split(".") # [header, payload, signature]
if len(parts) >= 2:
# base64url → добавляем padding ("==") на случай если длина не кратна 4
payload = base64.urlsafe_b64decode(parts[1] + "==")
return json.loads(payload).get("ClientID", "")
except Exception:
pass # битый токен — не критично, вернём пустую строку
return ""
def get_stand():
"""Определить стенд.
Эмуляция: "polygon_{dev|test|prod}" — для load_service_ids и трекера.
Облако: автоопределение или stand_name (dev/test/mock)."""
mode = get_mode()
if mode == "polygon":
return f"polygon_{get_polygon_stand()}"
token = get_token()
endpoint = current_app.config["NUBES_API_ENDPOINT"]
if endpoint not in STANDS:
s = stand_name(endpoint)
return s if s != "?" else "mock"
return stand_name(_cached_detect(token))
def get_token_info():
"""Извлечь {email, company, client_id} из JWT — для отображения в топбаре UI.
Возвращает dict с ключами: email, company, client_id.
Если JWT невалиден — возвращает пустой {}."""
token = get_token()
try:
parts = token.split(".")
if len(parts) >= 2:
payload = base64.urlsafe_b64decode(parts[1] + "==")
d = json.loads(payload)
return {
"email": d.get("email", ""),
"company": d.get("company_name", ""),
"client_id": d.get("ClientID", ""),
}
except Exception:
pass
return {}
def get_token_masked():
"""Маскированный env-токен для placeholder в форме: abc...xyz.
Используется ТОЛЬКО env-токен (не пользовательский!).
Если токен короче 8 символов — возвращает пустую строку."""
token = current_app.config["NUBES_API_TOKEN"]
if not token or len(token) < 8:
return ""
# Первые 4 символа + звёздочки + последние 4 символа
return token[:4] + "*" * (len(token) - 8) + token[-4:]