221 lines
9.8 KiB
Python
221 lines
9.8 KiB
Python
"""Отладочный скрипт: ищем реальный 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-клиент."
|
||
)
|