SFM/scripts/debug_access_token.py
2026-09-17 13:04:35 +03:00

221 lines
9.8 KiB
Python
Raw Permalink 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.

"""Отладочный скрипт: ищем реальный access_token (не ID-токен), затем
находим личное пространство пользователя в OpenCloud и пробуем туда
загрузить пустой тестовый .txt — полная сквозная проверка одним файлом.
Запускать на проде / там, где установлена raisa_streamlit_oauth
(LOCATION != local):
streamlit run scripts/debug_access_token.py
Смотрим по очереди:
1. Заголовки запроса (st.context.headers) — вдруг oauth2-proxy прокидывает
access_token туда (X-Forwarded-Access-Token / X-Auth-Request-Access-Token
и т.п., если настроен --pass-access-token).
2. st.session_state целиком — вдруг сырой токен лежит не под "token_data",
а под каким-то другим ключом.
3. get_jwt_token() — то, что уже пробовали через чат, для сравнения (должно
совпасть с тем, что видели раньше — typ: ID).
4. Личное пространство (Graph API GET /graph/v1.0/me/drives, driveType
"personal") — ищем его id по уже найденному токену.
5. Пробная загрузка пустого .txt прямо в корень найденного личного
пространства — кнопка, ничего не грузится без явного клика.
"""
from __future__ import annotations
import os
import sys
import tempfile
from pathlib import Path
import streamlit as st
from dotenv import load_dotenv
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from app.opencloud_access import ( # noqa: E402
OPENCLOUD_DOMAIN,
OpenCloudUploadError,
upload_local_file_to_opencloud,
)
load_dotenv()
st.set_page_config(page_title="Debug: access token", page_icon="🔍")
st.title("Поиск access_token")
st.warning(
"УСТАРЕЛО (16.09): подтверждено, что прод использует не Bearer/ID-токен "
"Keycloak, а App Token заказчика через HTTP Basic Auth (см. "
"app/opencloud_access.py и app/opencloud_token_store.py). Шаги 1–4 ниже "
"относятся к отвергнутому Bearer-пути и оставлены только как справка "
"по тому, как это выяснялось. Для шага 5 добавлено поле username — "
"тестировать нужно App Token'ом, а не Bearer-токеном из Keycloak."
)
st.caption(
"Отладочная страница, не часть прод-приложения. Смотрим заголовки, "
"session_state и get_jwt_token() в поисках сырого Bearer-токена для "
"OpenCloud, затем находим личное пространство и пробуем туда загрузить "
"тестовый файл."
)
try:
from raisa_streamlit_oauth import get_jwt_token, protect_application
protect_application()
except ImportError:
st.error(
"raisa_streamlit_oauth недоступна — этот скрипт нужно запускать там, "
"где библиотека установлена (LOCATION != local). Локально работать "
"не будет, там только mock-провайдер."
)
st.stop()
st.header("1. Заголовки запроса (st.context.headers)")
st.caption(
"Ищем что-то вроде X-Forwarded-Access-Token / X-Auth-Request-Access-Token "
"— если oauth2-proxy настроен с --pass-access-token, токен будет здесь."
)
found_token: str | None = None
try:
headers = dict(st.context.headers)
token_like = {
key: value
for key, value in headers.items()
if "token" in key.lower() or "auth" in key.lower()
}
if token_like:
st.success("Нашлись заголовки, похожие на токен:")
st.json(token_like)
else:
st.warning("Заголовков с 'token'/'auth' в названии не нашлось.")
with st.expander("Все заголовки (полный список)"):
st.json(headers)
authorization = headers.get("Authorization", "")
if authorization.lower().startswith("bearer "):
found_token = authorization[len("Bearer ") :].strip()
except AttributeError:
st.error(
"st.context.headers недоступен в этой версии Streamlit "
f"({st.__version__}) — нужна 1.37+."
)
st.header("2. st.session_state целиком")
st.caption(
"Ищем длинную строку вида 'eyJ...' под любым ключом — возможно, сырой "
"токен лежит не под 'token_data', а где-то ещё (например, как в старом "
"aurora_oauth было 'ST_OAUTH')."
)
session_dump = {}
for key in st.session_state.keys():
value = st.session_state[key]
# обрезаем на случай больших значений, чтобы страница не легла
session_dump[key] = str(value)[:500]
st.json(session_dump)
st_oauth = st.session_state.get("ST_OAUTH")
if not found_token and isinstance(st_oauth, dict):
value = st_oauth.get("access_token")
if value:
found_token = str(value)
st.header("3. get_jwt_token() — для сравнения с тем, что уже видели в чате")
try:
token_data = get_jwt_token()
st.write("typ:", token_data.get("typ"))
st.write("aud:", token_data.get("aud"))
with st.expander("Полный token_data"):
st.json(token_data)
except Exception as exc: # noqa: BLE001
st.error(f"get_jwt_token() упал с ошибкой: {exc}")
st.divider()
st.header("4. Личное пространство в OpenCloud (Graph API)")
st.caption(
"GET /graph/v1.0/me/drives, ищем запись с driveType == 'personal' — её "
"id и есть SPACE_ID для загрузки, без всякой личной папки по имени."
)
token_input = st.text_input(
"Токен для запроса (можно поправить руками, если найденный не тот)",
value=found_token or "",
type="password",
)
personal_drive: dict | None = None
if token_input:
if st.button("Найти личное пространство"):
import requests
try:
response = requests.get(
f"https://{OPENCLOUD_DOMAIN}/graph/v1.0/me/drives",
headers={"Authorization": f"Bearer {token_input}"},
timeout=30,
)
response.raise_for_status()
drives = response.json().get("value", [])
st.session_state["_debug_drives"] = drives
except Exception as exc: # noqa: BLE001
st.error(f"Запрос к Graph API не удался: {exc}")
else:
st.info("Нет токена — нечем сходить в Graph API.")
drives = st.session_state.get("_debug_drives", [])
if drives:
with st.expander(f"Все пространства ({len(drives)})"):
st.json(drives)
personal_candidates = [d for d in drives if d.get("driveType") == "personal"]
if personal_candidates:
personal_drive = personal_candidates[0]
st.success(
f"Личное пространство: {personal_drive.get('name')} "
f"(id: {personal_drive.get('id')})"
)
else:
st.warning("Среди пространств нет ни одного с driveType == 'personal'.")
st.header("5. Тестовая загрузка пустого .txt в личное пространство")
st.caption(
"Грузит plain-текстовый файл прямо в корень найденного личного "
"пространства (без подпапок) — через тот же код, что и прод "
"(app/opencloud_access.py). ВНИМАНИЕ: авторизация теперь Basic Auth — "
"в 'Токен' ниже нужен App Token (не Bearer), плюс отдельно username."
)
username_input = st.text_input(
"Username (для Basic Auth)",
value=os.getenv("SFM_OPENCLOUD_USERNAME", ""),
)
if personal_drive and token_input and username_input:
if st.button("Загрузить тестовый файл", type="primary"):
with tempfile.TemporaryDirectory() as tmp_dir:
local_file = Path(tmp_dir) / "sfm_debug_test.txt"
local_file.write_text(
"Тестовый файл из scripts/debug_access_token.py\n",
encoding="utf-8",
)
try:
url = upload_local_file_to_opencloud(
local_file,
space_id=personal_drive["id"],
folder_segments=(),
file_name="sfm_debug_test.txt",
username=username_input,
token=token_input,
)
except OpenCloudUploadError as exc:
st.error(f"Загрузка не удалась: {exc}")
else:
st.success("Загружено успешно.")
st.write(url)
else:
st.info("Сначала нужно найти личное пространство (шаг 4).")
st.divider()
st.caption(
"Если ни в заголовках, ни в session_state сырого access_token не "
"нашлось — придётся смотреть исходники библиотеки "
"(pip show -f raisa_streamlit_oauth покажет путь до файлов) или "
"спрашивать тех, кто настраивал oauth2-proxy/Keycloak-клиент."
)