diff --git a/README.md b/README.md index 5ddeafa..890bd27 100644 --- a/README.md +++ b/README.md @@ -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__ @@ -9,39 +9,37 @@ __sfm__ | Отдел | aurora | aurora | | Контакты | raykovmse@rshb.ru | raykovmse@rshb.ru | ----- -### Структура проекта: -![Структура проекта](/assets/image.jpg) -### Рабочий процесс: -1. Клонируем репозиторий -```commandline -git@:/.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--" -``` -4. Пишем свой код. Коммитим изменения -```commandline -git add . -git commit -m "" -``` -Как писать хорошие коммит сообщения: -https://www.conventionalcommits.org/ru/v1.0.0/ +## Архитектура -5. Пушим изменения в Gitlab -```commandline -git push origin +```text +main.py +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. Мерджите diff --git a/app/auth/mock_provider.py b/app/auth/mock_provider.py index 3278d62..460fc10 100644 --- a/app/auth/mock_provider.py +++ b/app/auth/mock_provider.py @@ -30,5 +30,5 @@ def get_jwt_token() -> dict[str, str] | None: "name": full_name or username or email, "realm_access": {"roles": ["local_dev"]}, } - return {"id_token": "local-dev-token"} + return st.session_state["local_mock_profile"] diff --git a/app/auth/providers.py b/app/auth/providers.py index 7370f54..5cc9682 100644 --- a/app/auth/providers.py +++ b/app/auth/providers.py @@ -1,11 +1,8 @@ from __future__ import annotations -from importlib.util import find_spec from typing import Any, Protocol -from streamlit_oauth import OAuth2Component - -from app.config import get_location, get_setting +from app.config import get_location from .mock_provider import get_jwt_token as get_jwt_token_local from .mock_provider import protect_application as protect_application_local @@ -34,33 +31,8 @@ class RaisaProvider: 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: if get_location() == "local": return LocalMockProvider() - if find_spec("raisa_streamlit_oauth") is not None: - return RaisaProvider() - return OAuthFallbackProvider() + return RaisaProvider() diff --git a/app/auth/service.py b/app/auth/service.py index 75d1081..6617c8a 100644 --- a/app/auth/service.py +++ b/app/auth/service.py @@ -1,12 +1,7 @@ from __future__ import annotations -from typing import Any - -import jwt import streamlit as st -from app.config import get_location, get_setting - from .providers import build_auth_provider @@ -20,31 +15,27 @@ class AuthService: if "user_profile" in st.session_state: return True - token_data = build_auth_provider().get_token_data() + try: + token_data = build_auth_provider().get_token_data() + except ImportError: + st.error( + "Библиотека raisa_streamlit_oauth недоступна. " + "Для локальной разработки установите LOCATION=local." + ) + return False + if not token_data: return False - id_token = token_data.get("id_token") - if not id_token: - st.error("Не удалось получить id_token из ответа авторизации.") + email = token_data.get("email") + if not email: + st.error( + "Не удалось идентифицировать пользователя: " + "в токене отсутствует поле email." + ) return False - profile = self._decode_id_token(id_token) st.session_state["token_data"] = token_data - st.session_state["user_profile"] = profile + st.session_state["user_profile"] = token_data 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}, - ) - diff --git a/app/config.py b/app/config.py index 0fa27f0..88d4358 100644 --- a/app/config.py +++ b/app/config.py @@ -3,17 +3,6 @@ from __future__ import annotations import os 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: @@ -23,10 +12,6 @@ def get_setting(name: str) -> str: 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: - return get_setting("LOCATION").lower() + return get_setting("LOCATION").lower() or "prod" diff --git a/app/ui.py b/app/ui.py index 8085137..e75bea5 100644 --- a/app/ui.py +++ b/app/ui.py @@ -2,19 +2,16 @@ from __future__ import annotations 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: with st.expander("Проверка env окружения"): - if st.button("Проверить OPENBAO__SETTINGS", use_container_width=True): - missing = get_missing_settings() - if missing: - st.error("Не заданы обязательные переменные:") - for name in missing: - st.code(f"{SETTINGS_PREFIX}{name} (резерв: {name})", language="text") - else: - st.success("Все обязательные переменные заданы.") + st.write(f"Текущий режим: `{get_location()}`") + st.code( + f"{SETTINGS_PREFIX}LOCATION=local # для локального mock-режима", + language="text", + ) def render_profile(profile: dict) -> None: @@ -24,7 +21,7 @@ def render_profile(profile: dict) -> None: { "email": profile.get("email"), "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", []), } ) diff --git a/requirements.txt b/requirements.txt index 18218c6..d751c80 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,2 @@ -openpyxl==3.1.2 -streamlit-oauth==0.1.8 -pyjwt[crypto]==2.8.0 -python-decouple==3.8 -black[all]==24.4.2 -flake8==7.0.0 -isort==5.13.2 +streamlit==1.46.1 +ruff==0.5.2