from __future__ import annotations import os import streamlit as st def resolve_user_id() -> str: profile = st.session_state.get("user_profile", {}) if not isinstance(profile, dict): return "anonymous|anonymous" email = str( profile.get("email") or profile.get("preferred_username") or "anonymous" ) full_name = str(profile.get("name") or profile.get("full_name") or "anonymous") return f"{email}|{full_name}" def resolve_opencloud_username() -> str | None: """preferred_username из Keycloak — им же заказчик регистрировал App Token в веб-интерфейсе OpenCloud (сверено вручную, совпадает без домена/регистра). Для локального теста (mock-провайдер) — fallback на .env. """ profile = st.session_state.get("user_profile", {}) if isinstance(profile, dict): value = profile.get("preferred_username") if value: return str(value) return os.getenv("SFM_OPENCLOUD_USERNAME") or None def resolve_opencloud_access_token() -> str | None: """Достаёт App Token заказчика для авторизации в OpenCloud (Basic Auth). ИСТОРИЯ (см. app/opencloud_access.py): изначально пытались использовать Bearer ID-токен Keycloak-клиента datalab — Graph API OpenCloud отвечал 401 (несовпадение audience). Подтверждено на проде 16.09: рабочий путь — HTTP Basic Auth с username (resolve_opencloud_username) и долгоживущим App Token, который заказчик сам создаёт в веб-интерфейсе OpenCloud. Токены хранятся зашифрованными в БД (app/opencloud_token_store.py), заказчики присылают их нам сами при онбординге. Приложение синхронное (Streamlit) — обёртка asyncio.run вокруг async store оправдана только частотой вызова (раз на запуск обработки), при росте нагрузки стоит пересмотреть. """ username = resolve_opencloud_username() if not username: return None import asyncio from app.opencloud_token_store import OpenCloudTokenStoreError, get_token try: return asyncio.run(get_token(username)) except OpenCloudTokenStoreError as exc: st.error(f"Ошибка доступа к токену OpenCloud: {exc}") return None except Exception as exc: # noqa: BLE001 # Раньше эта ветка тихо проглатывала ЛЮБУЮ ошибку (например, # несовпадение event loop'ов asyncpg между последовательными # asyncio.run() — см. app/opencloud_token_store.py) и превращала # её в безобидное "токен не найден", маскируя реальный сбой БД. # Показываем ошибку явно, fallback на .env — только если он там # реально есть (локальная разработка без поднятой БД). st.error(f"Ошибка доступа к БД токенов OpenCloud: {exc}") return os.getenv("SFM_OPENCLOUD_ACCESS_TOKEN") or None