правки
This commit is contained in:
parent
a295417287
commit
488b574366
68
README.md
68
README.md
@ -1,4 +1,4 @@
|
|||||||
[](https://github.com/psf/black) [](https://pycqa.github.io/isort/) [](https://github.com/pylint-dev/pylint)
|
[](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 |
|
||||||
-----
|
-----
|
||||||
### Структура проекта:
|
## Архитектура
|
||||||

|
|
||||||
### Рабочий процесс:
|
|
||||||
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. Мерджите
|
|
||||||
|
|||||||
@ -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"]
|
||||||
|
|
||||||
|
|||||||
@ -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()
|
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
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:
|
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},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|||||||
@ -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"
|
||||||
|
|
||||||
|
|||||||
17
app/ui.py
17
app/ui.py
@ -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", []),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user