правки

This commit is contained in:
Raykov-MS 2026-06-04 15:20:29 +03:00
parent a295417287
commit 488b574366
7 changed files with 62 additions and 124 deletions

View File

@ -1,4 +1,4 @@
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/) [![linting: pylint](https://img.shields.io/badge/linting-pylint-yellowgreen)](https://github.com/pylint-dev/pylint) [![linting: ruff](https://img.shields.io/badge/linting-ruff-f9c700)](https://github.com/astral-sh/ruff)
__________________________________________________________________________________________________________ __________________________________________________________________________________________________________
# sfm, v 0.0.1 # sfm, v 0.0.1
__sfm__ __sfm__
@ -9,39 +9,37 @@ __sfm__
| Отдел | aurora | aurora | | Отдел | aurora | aurora |
| Контакты | raykovmse@rshb.ru | raykovmse@rshb.ru | | Контакты | raykovmse@rshb.ru | raykovmse@rshb.ru |
----- -----
### Структура проекта: ## Архитектура
![Структура проекта](/assets/image.jpg)
### Рабочий процесс:
1. Клонируем репозиторий
```commandline
git@<gitlab_url>:<group_name>/<project_name>.git
```
2. Создаем виртуальное окружение
```commandline
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
```
Если что-то устанавливаем, не забываем обновлять список зависимостей
```commandline
pip freeze -l > requirements.txt
```
3. Создаем feature ветку
```commandline
git checkout -b "feature-<task_number>-<short_task_description>"
```
4. Пишем свой код. Коммитим изменения
```commandline
git add .
git commit -m "<commit_message>"
```
Как писать хорошие коммит сообщения:
https://www.conventionalcommits.org/ru/v1.0.0/
5. Пушим изменения в Gitlab ```text
```commandline main.py
git push origin app/
config.py # переменные окружения с префиксом OPENBAO__SETTINGS__
ui.py # ui-компоненты
auth/
service.py # orchestration auth/session
providers.py # local/mock и prod/raisa провайдеры
mock_provider.py # мок для локального запуска
```
## Режимы запуска
- `OPENBAO__SETTINGS__LOCATION=local` -> локальный mock вход через форму.
- `OPENBAO__SETTINGS__LOCATION=prod` (или пусто) -> вход через `raisa_streamlit_oauth`.
## Быстрый старт
```bash
python -m venv .venv
.\.venv\Scripts\activate
pip install -r requirements.txt
copy .env.example .env
streamlit run main.py
```
## Линт и формат
```bash
ruff check .
ruff format .
``` ```
6. Создаем Merge Request в master
7. Проходим код-ревью с коллегами
8. Мерджите

View File

@ -30,5 +30,5 @@ def get_jwt_token() -> dict[str, str] | None:
"name": full_name or username or email, "name": full_name or username or email,
"realm_access": {"roles": ["local_dev"]}, "realm_access": {"roles": ["local_dev"]},
} }
return {"id_token": "local-dev-token"} return st.session_state["local_mock_profile"]

View File

@ -1,11 +1,8 @@
from __future__ import annotations from __future__ import annotations
from importlib.util import find_spec
from typing import Any, Protocol from typing import Any, Protocol
from streamlit_oauth import OAuth2Component from app.config import get_location
from app.config import get_location, get_setting
from .mock_provider import get_jwt_token as get_jwt_token_local from .mock_provider import get_jwt_token as get_jwt_token_local
from .mock_provider import protect_application as protect_application_local from .mock_provider import protect_application as protect_application_local
@ -34,33 +31,8 @@ class RaisaProvider:
return get_jwt_token_raisa() return get_jwt_token_raisa()
class OAuthFallbackProvider:
def get_token_data(self) -> dict[str, Any] | None:
oauth2 = OAuth2Component(
client_id=get_setting("KEYCLOAK_CLIENT_ID"),
client_secret=get_setting("KEYCLOAK_CLIENT_SECRET"),
authorize_endpoint=get_setting("KEYCLOAK_AUTHORIZE_URL"),
token_endpoint=get_setting("KEYCLOAK_TOKEN_URL"),
refresh_token_endpoint=get_setting("KEYCLOAK_TOKEN_URL"),
revoke_token_endpoint=get_setting("KEYCLOAK_LOGOUT_URL"),
)
result = oauth2.authorize_button(
name="Войти через Keycloak",
redirect_uri=get_setting("KEYCLOAK_REDIRECT_URI"),
scope=get_setting("KEYCLOAK_SCOPE"),
key="keycloak-login",
use_container_width=True,
pkce="S256",
)
if result and "token" in result:
return result["token"]
return None
def build_auth_provider() -> AuthProvider: def build_auth_provider() -> AuthProvider:
if get_location() == "local": if get_location() == "local":
return LocalMockProvider() return LocalMockProvider()
if find_spec("raisa_streamlit_oauth") is not None:
return RaisaProvider() return RaisaProvider()
return OAuthFallbackProvider()

View File

@ -1,12 +1,7 @@
from __future__ import annotations from __future__ import annotations
from typing import Any
import jwt
import streamlit as st import streamlit as st
from app.config import get_location, get_setting
from .providers import build_auth_provider from .providers import build_auth_provider
@ -20,31 +15,27 @@ class AuthService:
if "user_profile" in st.session_state: if "user_profile" in st.session_state:
return True return True
try:
token_data = build_auth_provider().get_token_data() token_data = build_auth_provider().get_token_data()
except ImportError:
st.error(
"Библиотека raisa_streamlit_oauth недоступна. "
"Для локальной разработки установите LOCATION=local."
)
return False
if not token_data: if not token_data:
return False return False
id_token = token_data.get("id_token") email = token_data.get("email")
if not id_token: if not email:
st.error("Не удалось получить id_token из ответа авторизации.") st.error(
"Не удалось идентифицировать пользователя: "
"в токене отсутствует поле email."
)
return False return False
profile = self._decode_id_token(id_token)
st.session_state["token_data"] = token_data st.session_state["token_data"] = token_data
st.session_state["user_profile"] = profile st.session_state["user_profile"] = token_data
return True return True
def _decode_id_token(self, id_token: str) -> dict[str, Any]:
if get_location() == "local":
return st.session_state.get("local_mock_profile", {"email": "local@local"})
jwks_client = jwt.PyJWKClient(get_setting("KEYCLOAK_JWKS_URL"))
signing_key = jwks_client.get_signing_key_from_jwt(id_token)
return jwt.decode(
id_token,
signing_key.key,
algorithms=[get_setting("KEYCLOAK_JWT_ALGORITHM")],
audience=get_setting("KEYCLOAK_CLIENT_ID"),
options={"verify_exp": True},
)

View File

@ -3,17 +3,6 @@ from __future__ import annotations
import os import os
SETTINGS_PREFIX = "OPENBAO__SETTINGS__" SETTINGS_PREFIX = "OPENBAO__SETTINGS__"
REQUIRED_KEYCLOAK_SETTINGS = (
"KEYCLOAK_CLIENT_ID",
"KEYCLOAK_CLIENT_SECRET",
"KEYCLOAK_AUTHORIZE_URL",
"KEYCLOAK_TOKEN_URL",
"KEYCLOAK_LOGOUT_URL",
"KEYCLOAK_JWKS_URL",
"KEYCLOAK_REDIRECT_URI",
"KEYCLOAK_SCOPE",
"KEYCLOAK_JWT_ALGORITHM",
)
def get_setting(name: str) -> str: def get_setting(name: str) -> str:
@ -23,10 +12,6 @@ def get_setting(name: str) -> str:
return os.getenv(name, "").strip() return os.getenv(name, "").strip()
def get_missing_settings() -> list[str]:
return [name for name in REQUIRED_KEYCLOAK_SETTINGS if not get_setting(name)]
def get_location() -> str: def get_location() -> str:
return get_setting("LOCATION").lower() return get_setting("LOCATION").lower() or "prod"

View File

@ -2,19 +2,16 @@ from __future__ import annotations
import streamlit as st import streamlit as st
from app.config import SETTINGS_PREFIX, get_missing_settings from app.config import SETTINGS_PREFIX, get_location
def render_env_check() -> None: def render_env_check() -> None:
with st.expander("Проверка env окружения"): with st.expander("Проверка env окружения"):
if st.button("Проверить OPENBAO__SETTINGS", use_container_width=True): st.write(f"Текущий режим: `{get_location()}`")
missing = get_missing_settings() st.code(
if missing: f"{SETTINGS_PREFIX}LOCATION=local # для локального mock-режима",
st.error("Не заданы обязательные переменные:") language="text",
for name in missing: )
st.code(f"{SETTINGS_PREFIX}{name} (резерв: {name})", language="text")
else:
st.success("Все обязательные переменные заданы.")
def render_profile(profile: dict) -> None: def render_profile(profile: dict) -> None:
@ -24,7 +21,7 @@ def render_profile(profile: dict) -> None:
{ {
"email": profile.get("email"), "email": profile.get("email"),
"preferred_username": profile.get("preferred_username"), "preferred_username": profile.get("preferred_username"),
"name": profile.get("name"), "name": profile.get("name") or profile.get("full_name"),
"realm_roles": profile.get("realm_access", {}).get("roles", []), "realm_roles": profile.get("realm_access", {}).get("roles", []),
} }
) )

View File

@ -1,7 +1,2 @@
openpyxl==3.1.2 streamlit==1.46.1
streamlit-oauth==0.1.8 ruff==0.5.2
pyjwt[crypto]==2.8.0
python-decouple==3.8
black[all]==24.4.2
flake8==7.0.0
isort==5.13.2