Protect recipe API with bearer token
This commit is contained in:
@@ -330,6 +330,18 @@ hex вместо Base64. Узкий тест выявил проблему; ко
|
|||||||
Контрольный запрос с `PLAN/lekar.png` через `POST /gemini` на localhost ВМ:
|
Контрольный запрос с `PLAN/lekar.png` через `POST /gemini` на localhost ВМ:
|
||||||
HTTP 200.
|
HTTP 200.
|
||||||
|
|
||||||
|
## Защита recipe API
|
||||||
|
|
||||||
|
По команде пользователя добавлена Bearer-защита endpoint `POST /recipe`.
|
||||||
|
Приложение сравнивает заголовок `Authorization` со значением
|
||||||
|
`RECIPE_API_TOKEN` из EnvironmentFile; при отсутствии или неверном токене
|
||||||
|
возвращается HTTP 401. Health endpoint остаётся доступен без авторизации.
|
||||||
|
|
||||||
|
Добавлен `recipe_service/.gitignore`, исключающий `.env`, `*.env`,
|
||||||
|
`__pycache__` и `*.pyc`. В `.env.example` добавлено только имя настройки без
|
||||||
|
секретного значения. Реальный случайный токен будет храниться только на ВМ в
|
||||||
|
`/etc/recipe/recipe.env`; в git и HISTORY он не записывается.
|
||||||
|
|
||||||
Ответ модели:
|
Ответ модели:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -467,3 +479,20 @@ S. по 1т 1/р в день
|
|||||||
`thoughtsTokenCount` в ответе отсутствует, что подтверждает отсутствие
|
`thoughtsTokenCount` в ответе отсутствует, что подтверждает отсутствие
|
||||||
отдельно тарифицируемых reasoning-токенов в этом запросе. Usage: prompt 1119,
|
отдельно тарифицируемых reasoning-токенов в этом запросе. Usage: prompt 1119,
|
||||||
image 1102, text 17, candidate output 34, total 1153.
|
image 1102, text 17, candidate output 34, total 1153.
|
||||||
|
|
||||||
|
## Bearer-защита recipe API
|
||||||
|
|
||||||
|
Добавлена Bearer-защита `POST /recipe`: без заголовка или с неверным значением
|
||||||
|
`Authorization: Bearer ...` сервис возвращает HTTP 401. `/health` остаётся
|
||||||
|
доступен без авторизации.
|
||||||
|
|
||||||
|
Случайный токен установлен на ВМ 213 в `/etc/recipe/recipe.env` с правами
|
||||||
|
`0640` и владельцем `root:recipe`; значение токена не записывалось в git или
|
||||||
|
HISTORY. Добавлен `recipe_service/.gitignore` для `.env`, `*.env`,
|
||||||
|
`__pycache__` и `*.pyc`.
|
||||||
|
|
||||||
|
Синтаксис `recipe_service/app.py`, `git diff --check` и локальная проверка Flask
|
||||||
|
прошли. После перезапуска `recipe.service` запрос без токена получил HTTP 401.
|
||||||
|
Авторизованный запрос дошёл до Gemini, но получил HTTP 502: `API key not valid`.
|
||||||
|
Следовательно, Bearer-защита работает, а `GEMINI_API_KEY` в серверном env-файле
|
||||||
|
недействителен и требует замены.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
GEMINI_API_KEY=replace-with-secret
|
GEMINI_API_KEY=replace-with-secret
|
||||||
GEMINI_MODEL=gemini-3.6-flash
|
GEMINI_MODEL=gemini-3.6-flash
|
||||||
GEMINI_GENERATION_CONFIG={"temperature":0,"maxOutputTokens":300,"thinkingConfig":{"thinkingLevel":"minimal"}}
|
GEMINI_GENERATION_CONFIG={"temperature":0,"maxOutputTokens":300,"thinkingConfig":{"thinkingLevel":"minimal"}}
|
||||||
|
RECIPE_API_TOKEN=replace-with-random-token
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
.env
|
||||||
|
*.env
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
@@ -3,11 +3,14 @@
|
|||||||
Минимальный Flask-сервис на ВМ `5.172.178.213`.
|
Минимальный Flask-сервис на ВМ `5.172.178.213`.
|
||||||
|
|
||||||
Вход: `POST /recipe` в формате `multipart/form-data` с полями `image` и
|
Вход: `POST /recipe` в формате `multipart/form-data` с полями `image` и
|
||||||
`prompt`. Изображение: JPEG/PNG/WEBP, не более 10 MB.
|
`prompt`. Требуется заголовок `Authorization: Bearer <RECIPE_API_TOKEN>`.
|
||||||
|
Изображение: JPEG/PNG/WEBP, не более 10 MB.
|
||||||
|
|
||||||
Ключ Gemini и JSON-строка `GEMINI_GENERATION_CONFIG` находятся только в
|
Ключ Gemini и JSON-строка `GEMINI_GENERATION_CONFIG` находятся только в
|
||||||
серверном EnvironmentFile. Сервис передаёт изображение, prompt и настройки в
|
серверном EnvironmentFile. Сервис передаёт изображение, prompt и настройки в
|
||||||
Gemini и возвращает `text` и `usage`; разбор рецепта выполняется позднее.
|
Gemini и возвращает `text` и `usage`; разбор рецепта выполняется позднее.
|
||||||
|
Токен `RECIPE_API_TOKEN` также хранится только в EnvironmentFile и не
|
||||||
|
передаётся в Gemini.
|
||||||
|
|
||||||
Сервис изолирован от `elmer`: отдельные каталог, virtualenv, пользователь,
|
Сервис изолирован от `elmer`: отдельные каталог, virtualenv, пользователь,
|
||||||
порт, systemd-юнит и настройки nginx. Изображения на диск не сохраняются.
|
порт, systemd-юнит и настройки nginx. Изображения на диск не сохраняются.
|
||||||
@@ -25,6 +25,12 @@ def settings() -> tuple[str, dict]:
|
|||||||
return key, config
|
return key, config
|
||||||
|
|
||||||
|
|
||||||
|
def authorized() -> bool:
|
||||||
|
expected = os.environ.get("RECIPE_API_TOKEN")
|
||||||
|
authorization = request.headers.get("Authorization", "")
|
||||||
|
return bool(expected and authorization == f"Bearer {expected}")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
def health():
|
def health():
|
||||||
return jsonify(status="ok")
|
return jsonify(status="ok")
|
||||||
@@ -32,6 +38,8 @@ def health():
|
|||||||
|
|
||||||
@app.post("/recipe")
|
@app.post("/recipe")
|
||||||
def recipe():
|
def recipe():
|
||||||
|
if not authorized():
|
||||||
|
return jsonify(error="unauthorized"), 401
|
||||||
image = request.files.get("image")
|
image = request.files.get("image")
|
||||||
prompt = request.form.get("prompt")
|
prompt = request.form.get("prompt")
|
||||||
if image is None or not prompt:
|
if image is None or not prompt:
|
||||||
|
|||||||
Reference in New Issue
Block a user