fix: CORS при DEBUG — * без allow_credentials (ревью R-11, #9)
`*` вместе с credentials спецификация CORS запрещает (Starlette тогда отражает Origin запроса); фронт передаёт токен заголовком Authorization, cookie не нужны. README «Конфигурация» уточнён. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
parent
2c83005564
commit
e8d14edfbb
@ -450,7 +450,7 @@ email без учёта регистра проверяет только сер
|
|||||||
| `ENV_FILE` | `.env` | файл окружения (относительно `api/`) |
|
| `ENV_FILE` | `.env` | файл окружения (относительно `api/`) |
|
||||||
| `DEBUG` | `false` | режим разработки. `true` необходим dev-эмитенту токенов и `make seed-dev`, включает документацию API (`/docs`, `/redoc`, `/openapi.json`) и CORS `*`; при `false` документации нет (404), CORS — из `CORS_ORIGINS`, пакеты РАИСА подгружаются условно |
|
| `DEBUG` | `false` | режим разработки. `true` необходим dev-эмитенту токенов и `make seed-dev`, включает документацию API (`/docs`, `/redoc`, `/openapi.json`) и CORS `*`; при `false` документации нет (404), CORS — из `CORS_ORIGINS`, пакеты РАИСА подгружаются условно |
|
||||||
| `FORWARDED_ALLOW_IPS` | `127.0.0.1` | доверенные прокси для `X-Forwarded-For`/`X-Forwarded-Proto` (`ProxyHeadersMiddleware`): адреса через запятую или `*`. Адрес клиента (`audit_event.ip_address`) берётся из заголовка только от них, от остальных заголовок игнорируется. На платформе — адреса ingress/балансировщика (OQ-65) |
|
| `FORWARDED_ALLOW_IPS` | `127.0.0.1` | доверенные прокси для `X-Forwarded-For`/`X-Forwarded-Proto` (`ProxyHeadersMiddleware`): адреса через запятую или `*`. Адрес клиента (`audit_event.ip_address`) берётся из заголовка только от них, от остальных заголовок игнорируется. На платформе — адреса ingress/балансировщика (OQ-65) |
|
||||||
| `CORS_ORIGINS` | пусто | при `DEBUG=false` — источники, которым разрешены кросс-доменные запросы: строка через запятую (`https://a.example.ru,https://b.example.ru`) или JSON-список. Пусто — кросс-доменные запросы запрещены (middleware CORS не подключается); `allow_credentials` — только для явного списка без `*`. При `DEBUG=true` не используется (всегда `*`) |
|
| `CORS_ORIGINS` | пусто | при `DEBUG=false` — источники, которым разрешены кросс-доменные запросы: строка через запятую (`https://a.example.ru,https://b.example.ru`) или JSON-список. Пусто — кросс-доменные запросы запрещены (middleware CORS не подключается); `allow_credentials` — только для явного списка без `*`. При `DEBUG=true` не используется (всегда `*` без credentials: токен передаётся заголовком, cookie не нужны) |
|
||||||
| `SERVER__HOST`, `SERVER__PORT` | `0.0.0.0`, `8000` | адрес и порт сервера (`run_server.py`); в `.env.example` — `127.0.0.1` (dev-запуск только на loopback) |
|
| `SERVER__HOST`, `SERVER__PORT` | `0.0.0.0`, `8000` | адрес и порт сервера (`run_server.py`); в `.env.example` — `127.0.0.1` (dev-запуск только на loopback) |
|
||||||
| `ROOT_PATH` | `/` | корневой путь за прокси (должен совпадать с `REACT_APP_ROOT_PATH` фронта) |
|
| `ROOT_PATH` | `/` | корневой путь за прокси (должен совпадать с `REACT_APP_ROOT_PATH` фронта) |
|
||||||
| `VERSION` | `1.0.0` | версия в `/healthcheck` и OpenAPI |
|
| `VERSION` | `1.0.0` | версия в `/healthcheck` и OpenAPI |
|
||||||
|
|||||||
@ -191,13 +191,14 @@ async def lifespan(app: FastAPI):
|
|||||||
|
|
||||||
|
|
||||||
def cors_options(app_settings: Settings) -> dict[str, Any] | None:
|
def cors_options(app_settings: Settings) -> dict[str, Any] | None:
|
||||||
"""Источники CORS (OQ-65): DEBUG — `*` с credentials, как в шаблоне; иначе — `CORS_ORIGINS`.
|
"""Источники CORS (OQ-65): DEBUG — `*` без credentials (ревью R-11; токен — в заголовке, cookie не нужны);
|
||||||
|
иначе — `CORS_ORIGINS`.
|
||||||
|
|
||||||
Пустой `CORS_ORIGINS` вне DEBUG — `None`: middleware не подключается, браузер кросс-доменные запросы не выполнит.
|
Пустой `CORS_ORIGINS` вне DEBUG — `None`: middleware не подключается, браузер кросс-доменные запросы не выполнит.
|
||||||
`allow_credentials` вне DEBUG — только для явного списка без `*` (спецификация CORS запрещает `*` с credentials).
|
`allow_credentials` вне DEBUG — только для явного списка без `*` (спецификация CORS запрещает `*` с credentials).
|
||||||
"""
|
"""
|
||||||
if app_settings.DEBUG:
|
if app_settings.DEBUG:
|
||||||
return {"allow_origins": ["*"], "allow_credentials": True}
|
return {"allow_origins": ["*"], "allow_credentials": False}
|
||||||
origins = list(app_settings.CORS_ORIGINS)
|
origins = list(app_settings.CORS_ORIGINS)
|
||||||
if not origins:
|
if not origins:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@ -481,7 +481,8 @@ async def test_cors_debug_allows_any_origin(tmp_path: Any) -> None:
|
|||||||
response = await _request(_app_with(tmp_path, DEBUG=True), "OPTIONS", "/healthcheck", headers=_preflight_headers())
|
response = await _request(_app_with(tmp_path, DEBUG=True), "OPTIONS", "/healthcheck", headers=_preflight_headers())
|
||||||
|
|
||||||
assert response.status_code == HTTPStatus.OK
|
assert response.status_code == HTTPStatus.OK
|
||||||
assert response.headers["access-control-allow-origin"] in {"*", ORIGIN}
|
assert response.headers["access-control-allow-origin"] == "*"
|
||||||
|
assert "access-control-allow-credentials" not in response.headers # `*` без credentials (ревью R-11)
|
||||||
|
|
||||||
|
|
||||||
async def test_cors_prod_without_origins_denies_cross_origin(tmp_path: Any) -> None:
|
async def test_cors_prod_without_origins_denies_cross_origin(tmp_path: Any) -> None:
|
||||||
@ -525,7 +526,7 @@ def test_cors_options_rules() -> None:
|
|||||||
base = app_module.settings
|
base = app_module.settings
|
||||||
assert app_module.cors_options(base.model_copy(update={"DEBUG": True, "CORS_ORIGINS": []})) == {
|
assert app_module.cors_options(base.model_copy(update={"DEBUG": True, "CORS_ORIGINS": []})) == {
|
||||||
"allow_origins": ["*"],
|
"allow_origins": ["*"],
|
||||||
"allow_credentials": True,
|
"allow_credentials": False,
|
||||||
}
|
}
|
||||||
assert app_module.cors_options(base.model_copy(update={"DEBUG": False, "CORS_ORIGINS": []})) is None
|
assert app_module.cors_options(base.model_copy(update={"DEBUG": False, "CORS_ORIGINS": []})) is None
|
||||||
assert app_module.cors_options(base.model_copy(update={"DEBUG": False, "CORS_ORIGINS": [ORIGIN]})) == {
|
assert app_module.cors_options(base.model_copy(update={"DEBUG": False, "CORS_ORIGINS": [ORIGIN]})) == {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user