Files
ss-tools/backend/src/plugins/llm_analysis/scripts/superset_auth_diag.py
2026-05-29 16:15:41 +03:00

469 lines
21 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.

#!/usr/bin/env python3
"""
Superset Auth Diagnostics Script — Extended v3
Проверяет все доступные способы аутентификации в Superset
ПОЛНЫМ ЦИКЛОМ: логин → сессия → загрузка дашборда → верификация контента.
Использование:
python3 superset_auth_diag.py --url https://superset.example.com --username admin
Опции:
--insecure, -k отключить проверку SSL
--dashboard-id конкретный дашборд для теста
Требует: pip install httpx
"""
import argparse
import json
import re
import sys
from getpass import getpass
from urllib.parse import urlparse
try:
import httpx
except ImportError:
print("Установите httpx: pip install httpx")
sys.exit(1)
# ── helpers ──────────────────────────────────────────────────
def banner(msg):
print(f"\n{'='*60}")
print(f" {msg}")
print(f"{'='*60}")
def ok(msg):
print(f"{msg}")
def fail(msg):
print(f"{msg}")
def warn(msg):
print(f" ⚠️ {msg}")
def section(msg):
print(f"\n --- {msg} ---")
def http_client(verify: bool = True) -> httpx.Client:
import warnings
if not verify:
warnings.filterwarnings("ignore", message=".*verify=False.*", category=UserWarning)
return httpx.Client(verify=verify, timeout=30, follow_redirects=False)
def extract_session_cookie(set_cookie_header: str) -> dict | None:
"""Извлекает session=... из Set-Cookie заголовка."""
if not set_cookie_header:
return None
for part in set_cookie_header.split(";"):
part = part.strip()
if part.startswith("session="):
value = part.split("=", 1)[1]
return {"name": "session", "value": value}
return None
# ── 1. Reachability ──────────────────────────────────────────
def check_reachability(base_url: str, verify: bool) -> bool:
banner("1. Доступность Superset")
with http_client(verify) as c:
try:
r = c.get(f"{base_url}/health/")
ok(f"GET /health/ → HTTP {r.status_code}")
return True
except httpx.RequestError:
pass
try:
r = c.get(base_url)
ok(f"GET {base_url} → HTTP {r.status_code}")
return True
except httpx.RequestError as e:
fail(f"Недоступен: {e}")
return False
# ── 2. JWT Login ─────────────────────────────────────────────
def check_jwt_login(base_url: str, username: str, password: str, verify: bool) -> str | None:
banner("2. JWT API Login")
with http_client(verify) as c:
for provider in ("db", "ldap"):
try:
r = c.post(
f"{base_url}/api/v1/security/login",
json={"username": username, "password": password, "provider": provider, "refresh": True},
)
data = r.json() if r.text else {}
if r.status_code == 200 and data.get("access_token"):
ok(f"provider='{provider}' → JWT OK")
if data.get("refresh_token"):
ok(f" Refresh token OK")
return data["access_token"]
fail(f"provider='{provider}' → HTTP {r.status_code}: {str(data)[:200]}")
except Exception as e:
fail(f"provider='{provider}' → ошибка: {e}")
return None
# ── 3. JWT endpoints ─────────────────────────────────────────
def check_jwt_endpoints(base_url: str, token: str, verify: bool):
banner("3. JWT — доступ к API endpoints")
headers = {"Authorization": f"Bearer {token}"}
with http_client(verify) as c:
for path in ("/api/v1/me/", "/api/v1/dashboard/", "/api/v1/security/csrf_token/"):
try:
r = c.get(f"{base_url}{path}", headers=headers)
ok(f"GET {path} → HTTP {r.status_code}") if r.status_code in (200, 201) \
else fail(f"GET {path} → HTTP {r.status_code}")
except Exception as e:
fail(f"GET {path} → ошибка: {e}")
# ── 3b. CSRF token + session cookie ─────────────────────────
def check_csrf_session(base_url: str, token: str, verify: bool) -> tuple[str | None, dict | None]:
"""
GET /api/v1/security/csrf_token/ с JWT.
Возвращает (csrf_token, session_cookie_dict).
"""
section("3b. CSRF token + Set-Cookie session")
csrf_token = None
csrf_session_cookie = None
with http_client(verify) as c:
try:
r = c.get(f"{base_url}/api/v1/security/csrf_token/",
headers={"Authorization": f"Bearer {token}"})
data = r.json() if r.text else {}
if r.status_code == 200:
csrf_token = (data.get("result") or data.get("csrf_token") or "").strip('"')
if csrf_token:
ok(f"CSRF token: {csrf_token[:30]}...")
# Извлекаем session cookie из Set-Cookie
set_cookie = r.headers.get("set-cookie", "")
csrf_session = extract_session_cookie(set_cookie)
if csrf_session:
ok(f"Session cookie с CSRF: {csrf_session['value'][:30]}...")
csrf_session_cookie = csrf_session
else:
warn("Set-Cookie с session не найден в ответе CSRF endpoint")
else:
fail(f"GET csrf_token/ → HTTP {r.status_code}")
except Exception as e:
warn(f"GET csrf_token/ ошибка: {e}")
# Также проверим, есть ли session cookie от GET / (anonymous)
section("3c. GET / (anonymous) — Set-Cookie")
with http_client(verify) as c:
try:
r = c.get(base_url, follow_redirects=False)
sc = r.headers.get("set-cookie", "")
if "session" in sc:
sess = extract_session_cookie(sc)
ok(f"GET / → HTTP {r.status_code}, session cookie есть (anonymous)")
if sess:
print(f" session: {sess['value'][:40]}...")
else:
warn(f"GET / → HTTP {r.status_code}, session cookie НЕТ")
except Exception as e:
warn(f"GET / ошибка: {e}")
return csrf_token, csrf_session_cookie
# ── 4. Guest Token ───────────────────────────────────────────
def check_guest_token(base_url: str, token: str,
csrf_token: str | None, csrf_session: dict | None,
verify: bool, dashboard_ids: list[str] | None) -> list[str] | None:
banner("4. Guest Token")
bearer = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
with http_client(verify) as c:
# Получаем список дашбордов если не задан
if not dashboard_ids:
section("Получаем список дашбордов")
try:
r = c.get(f"{base_url}/api/v1/dashboard/", headers=bearer)
data = r.json() if r.text else {}
if r.status_code == 200:
dashboards = data.get("result", [])
ok(f"Найдено дашбордов: {len(dashboards)}")
for d in dashboards[:3]:
print(f" id={d['id']} {d.get('dashboard_title','')}")
dashboard_ids = [str(d["id"]) for d in dashboards[:1]]
else:
warn(f"HTTP {r.status_code}, используем dashboard_id=1")
dashboard_ids = ["1"]
except Exception as e:
warn(f"Ошибка: {e}, используем dashboard_id=1")
dashboard_ids = ["1"]
for did in dashboard_ids:
payload = {
"user": {"username": "admin"},
"resources": [{"type": "dashboard", "id": did}],
"rls": [],
}
# Способ 1: только JWT (без CSRF)
try:
r = c.post(f"{base_url}/api/v1/security/guest_token/", headers=bearer, json=payload)
if r.status_code == 200:
ok(f"dashboard #{did}: guest token (без CSRF) 🎉")
continue
warn(f"dashboard #{did}: HTTP {r.status_code} (без CSRF)")
except Exception as e:
warn(f"dashboard #{did}: ошибка (без CSRF): {e}")
# Способ 2: JWT + X-CSRFToken header
if csrf_token:
h = {**bearer, "X-CSRFToken": csrf_token, "Referer": f"{base_url}/"}
try:
r = c.post(f"{base_url}/api/v1/security/guest_token/", headers=h, json=payload)
if r.status_code == 200:
ok(f"dashboard #{did}: guest token (CSRF header) 🎉")
continue
warn(f"dashboard #{did}: HTTP {r.status_code} (CSRF header): {r.text[:100]}")
except Exception as e:
warn(f"dashboard #{did}: ошибка (CSRF header): {e}")
# Способ 3: JWT + X-CSRFToken + session cookie (из CSRF endpoint)
if csrf_token and csrf_session:
h = {**bearer, "X-CSRFToken": csrf_token, "Referer": f"{base_url}/"}
try:
print(f"\n >>> DEBUG: sending guest_token request...")
print(f" >>> X-CSRFToken: {csrf_token[:20]}...")
print(f" >>> Cookie: session={csrf_session['value'][:20]}...")
r = c.post(f"{base_url}/api/v1/security/guest_token/",
headers=h, json=payload,
cookies={"session": csrf_session["value"]})
print(f" >>> Response: HTTP {r.status_code}")
print(f" >>> Set-Cookie: {r.headers.get('set-cookie', 'NONE')[:100]}")
if r.status_code == 200:
ok(f"dashboard #{did}: guest token (JWT+CSRF+sess) 🎉")
continue
else:
print(f" >>> Body: {r.text[:200]}")
warn(f"dashboard #{did}: HTTP {r.status_code} (JWT+CSRF+sess)")
except Exception as e:
warn(f"dashboard #{did}: ошибка (JWT+CSRF+sess): {e}")
else:
warn(f"dashboard #{did}: пропускаем JWT+CSRF+sess — нет csrf_token или csrf_session")
return dashboard_ids
# ── 5. Form Login ────────────────────────────────────────────
def check_form_login(base_url: str, username: str, password: str, verify: bool) -> str | None:
"""POST /login/, возвращает сырой Set-Cookie или None."""
banner("5. Form Login (POST /login/)")
with http_client(verify) as c:
# GET /login/ — что показывает
try:
r_get = c.get(f"{base_url}/login/", follow_redirects=False)
warn(f"GET /login/ → HTTP {r_get.status_code}")
if r_get.status_code in (301, 302):
loc = r_get.headers.get("location", "")
warn(f" Редирект на: {loc.split('?')[0]}")
if "adfs" in loc.lower():
warn(" ADFS — db auth отключён на странице, POST может не работать")
except Exception as e:
warn(f"GET /login/ ошибка: {e}")
# POST с credentials
try:
r = c.post(f"{base_url}/login/",
data={"username": username, "password": password},
follow_redirects=False)
set_cookie = r.headers.get("set-cookie", "")
session_cookie = extract_session_cookie(set_cookie)
if session_cookie:
ok(f"POST /login/ → HTTP {r.status_code}, session cookie получена")
print(f" session: {session_cookie['value'][:40]}...")
return set_cookie
if r.status_code in (301, 302):
loc = r.headers.get("location", "")
if "/login" not in loc:
ok(f"POST /login/ → HTTP {r.status_code}{loc.split('?')[0]} — login успешен")
if set_cookie:
return set_cookie
fail(f"POST /login/ → редирект на login")
else:
fail(f"POST /login/ → HTTP {r.status_code}, session не найдена")
except Exception as e:
fail(f"POST /login/ ошибка: {e}")
return None
# ── 6. Session Cookie → Dashboard ────────────────────────────
def check_session_to_dashboard(base_url: str, set_cookie_header: str | None,
verify: bool, dashboard_ids: list[str] | None):
banner("6. Session Cookie → Dashboard (ПОЛНЫЙ ЦИКЛ)")
if not set_cookie_header:
fail("Нет session cookie — проверка невозможна")
return
session_cookie = extract_session_cookie(set_cookie_header)
if not session_cookie:
fail("Не удалось извлечь session cookie")
return
with http_client(verify) as c:
# 6a. Главная страница
section("6a. Главная страница с session cookie")
try:
r = c.get(base_url, cookies={"session": session_cookie["value"]}, follow_redirects=False)
if r.status_code == 200:
ok(f"GET / → HTTP 200, SPA загружен")
elif r.status_code in (301, 302):
loc = r.headers.get("location", "")
if "login" in loc.lower():
fail(f"GET / → редирект на login — session cookie НЕ РАБОТАЕТ")
else:
ok(f"GET / → HTTP {r.status_code}{loc.split('?')[0]} (редирект, не login)")
else:
warn(f"GET / → HTTP {r.status_code}")
except Exception as e:
fail(f"GET / ошибка: {e}")
# 6b. Dashboard
section("6b. Dashboard с session cookie")
if not dashboard_ids:
dashboard_ids = ["1"]
for did in dashboard_ids[:2]:
dash_url = f"{base_url}/superset/dashboard/{did}/?standalone=true"
try:
r = c.get(dash_url, cookies={"session": session_cookie["value"]}, follow_redirects=False)
if r.status_code == 200:
ok(f"dashboard #{did} → HTTP 200 — ДАШБОРД ЗАГРУЖЕН 🎉")
has_spa = "spa.html" in r.text or "bootstrap" in r.text
ok(" SPA/bootstrap_data найден") if has_spa else warn(" SPA/bootstrap_data НЕ найден")
print(f" Размер HTML: {len(r.text)} bytes")
elif r.status_code in (301, 302):
loc = r.headers.get("location", "")
fail(f"dashboard #{did} → редирект на {loc.split('?')[0]} — АУТЕНТИФИКАЦИЯ НЕ РАБОТАЕТ ❌")
elif r.status_code == 404:
fail(f"dashboard #{did} → HTTP 404 — дашборд не найден")
else:
warn(f"dashboard #{did} → HTTP {r.status_code}")
except Exception as e:
fail(f"dashboard #{did} → ошибка: {e}")
# ── 7. JWT → UI dashboard ────────────────────────────────────
def check_jwt_ui(base_url: str, token: str, verify: bool, dashboard_ids: list[str] | None):
banner("7. JWT Bearer → Dashboard UI (без session cookie)")
if not dashboard_ids:
dashboard_ids = ["1"]
with http_client(verify) as c:
for did in dashboard_ids[:1]:
try:
r = c.get(f"{base_url}/superset/dashboard/{did}/?standalone=true",
headers={"Authorization": f"Bearer {token}"},
follow_redirects=False)
if r.status_code == 200:
ok(f"dashboard #{did} → HTTP 200 — JWT РАБОТАЕТ ДЛЯ UI!")
elif r.status_code in (301, 302):
loc = r.headers.get("location", "")
fail(f"dashboard #{did} → редирект на {loc.split('?')[0]} — JWT НЕ РАБОТАЕТ")
else:
warn(f"dashboard #{did} → HTTP {r.status_code}")
except Exception as e:
warn(f"dashboard #{did} → ошибка: {e}")
# ── MAIN ──────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(description="Superset Auth Diagnostics (Extended v3)")
parser.add_argument("--url", required=True, help="Superset base URL")
parser.add_argument("--username", default="admin", help="Username")
parser.add_argument("--password", help="Password (будет запрошен)")
parser.add_argument("--dashboard-id", help="ID дашборда для теста")
parser.add_argument("--insecure", "-k", action="store_true", help="Отключить проверку SSL")
args = parser.parse_args()
base_url = args.url.rstrip("/")
username = args.username
password = args.password or getpass(f"Пароль для {username}: ")
verify = not args.insecure
dashboard_ids = [args.dashboard_id] if args.dashboard_id else None
if not verify:
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
print(f"\n🔍 Superset Auth Diagnostics (Extended v3)")
print(f" URL: {base_url}")
print(f" Username: {username}")
print(f" SSL: {'ON' if verify else 'OFF (--insecure)'}")
# 1. Reachability
if not check_reachability(base_url, verify):
print("\n❌ Superset недоступен.")
sys.exit(1)
# 2. JWT Login
jwt_token = check_jwt_login(base_url, username, password, verify)
csrf_token = None
csrf_session_cookie = None
if jwt_token:
# 3. JWT endpoints
check_jwt_endpoints(base_url, jwt_token, verify)
# 3b. CSRF token + session cookie
csrf_token, csrf_session_cookie = check_csrf_session(base_url, jwt_token, verify)
# 4. Guest Token
dashboard_ids = check_guest_token(base_url, jwt_token, csrf_token, csrf_session_cookie,
verify, dashboard_ids)
# 7. JWT → UI
check_jwt_ui(base_url, jwt_token, verify, dashboard_ids)
# 5. Form Login
session_cookie = check_form_login(base_url, username, password, verify)
# 6. Session → Dashboard
check_session_to_dashboard(base_url, session_cookie, verify, dashboard_ids)
# ── ИТОГ ──
banner("РЕЗЮМЕ")
jwt_ok = jwt_token is not None
form_ok = session_cookie is not None
ok("JWT API Login: РАБОТАЕТ") if jwt_ok else fail("JWT API Login: НЕ РАБОТАЕТ")
ok("Form Login: РАБОТАЕТ") if form_ok else fail("Form Login: НЕ РАБОТАЕТ")
print()
if jwt_ok and form_ok:
print("🏆 ОБА ПУТИ РАБОТАЮТ")
print()
print(" Реализация ScreenshotService:")
print(" 1. POST /api/v1/security/login → JWT")
print(" 2. POST /login/ через request_context → session cookie")
print(" 3. context.add_cookies([session_cookie])")
print(" 4. page.goto('/superset/dashboard/{id}/?standalone=true')")
print(" 5. Скриншот")
elif jwt_ok:
print("⚠️ Только JWT — нужен EMBEDDED_SUPERSET")
else:
print("❌ НИ ОДИН МЕТОД НЕ РАБОТАЕТ")
if __name__ == "__main__":
main()