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