Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions changelog.d/127.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
**`UserSessionDep` requires a session with a user.** It now resolves through
`require_user_session`, like `AuthenticatedSession`, so an anonymous session
gets `401` instead of passing. Routes that should keep admitting anonymous
sessions use `SessionDep`.
8 changes: 5 additions & 3 deletions docs/SESSION.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,10 @@ its annotated form `AuthenticatedSession`), which also answers `401` when `sessi
annotated form of `get_session`) is enough there, and `/public` uses `OptionalSession`
(`get_optional_session`), which gives `None` instead of answering `401`.

`UserSessionDep` does not check for a user despite its name; it is an alias of `SessionDep`
until 0.4.0, which is planned to make it require one.
`UserSessionDep` is the same as `AuthenticatedSession`: it answers `401` for an anonymous
session. Before 0.4.0 it was an alias of `SessionDep` and admitted anonymous sessions; use
`SessionDep` where that is what a route needs (see
[Migrating to 0.4.0](MIGRATING_0_4.md#user-session-dep)).

Under `FastAPICacheXSessionMiddleware`, log a user in with `await login(request, user)`. It
attaches the `SessionUser` that `require_user_session` / `AuthenticatedSession` check, under a
Expand Down Expand Up @@ -614,7 +616,7 @@ from fastapi_cachex.session.dependencies import (
OptionalSession, # Session | None
RequiredSession, # Session
SessionDep, # Session
UserSessionDep, # Session; anonymous sessions pass too, see above
UserSessionDep, # same as AuthenticatedSession since 0.4.0
AuthenticatedSession, # Session with a user (require_user_session)
SessionManagerDep, # SessionManager
)
Expand Down
5 changes: 2 additions & 3 deletions fastapi_cachex/session/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,9 +378,8 @@ async def login(manager: SessionManagerDep, client_ip: ClientIPDep):
OptionalSession = Annotated[Session | None, Depends(get_optional_session)]
RequiredSession = Annotated[Session, Depends(get_session)]
SessionDep = Annotated[Session, Depends(get_session)]
# Despite its name, UserSessionDep accepts anonymous sessions too; making it
# require a user is a breaking change planned for 0.4.0. Use AuthenticatedSession.
UserSessionDep = Annotated[Session, Depends(get_session)]
AuthenticatedSession = Annotated[Session, Depends(require_user_session)]
# Same as AuthenticatedSession; it admitted anonymous sessions before 0.4.0.
UserSessionDep = Annotated[Session, Depends(require_user_session)]
SessionManagerDep = Annotated["SessionManager", Depends(get_session_manager)]
ClientIPDep = Annotated[str | None, Depends(get_session_client_ip)]
4 changes: 2 additions & 2 deletions i18n/zh-TW/docs/SESSION.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ app.add_middleware(FastAPICacheXSessionMiddleware) # 從 proxy 取得

`get_session` 也接受這種 Session,因此它只能證明請求帶著「某個」Session,而不能證明有人登入。任何訪客只要進入會寫入 `request.session` 的路由(購物車、CSRF 值),就會得到一個。需要已登入使用者的路由,請改用 `require_user_session`(或其型別註記形式 `AuthenticatedSession`)保護,它在 `session.user` 為 `None` 時同樣回應 `401`,上方的 `/profile` 就是這樣做的。`/logout` 只會刪除 Session,因此使用 `SessionDep`(`get_session` 的型別註記形式)就足夠;`/public` 則使用 `OptionalSession`(`get_optional_session`),沒有 Session 時得到 `None`,而不是回應 `401`。

`UserSessionDep` 雖然名稱如此,卻不會檢查使用者;在 0.4.0 之前它是 `SessionDep` 的別名,0.4.0 預計改為要求使用者。
`UserSessionDep` 與 `AuthenticatedSession` 相同:匿名 Session 會得到 `401`。0.4.0 之前它是 `SessionDep` 的別名,也接受匿名 Session;路由確實需要接受匿名 Session 時,請使用 `SessionDep`(見[遷移至 0.4.0](MIGRATING_0_4.md#user-session-dep))。

在 `FastAPICacheXSessionMiddleware` 底下,請以 `await login(request, user)` 讓使用者登入。它會以新的 Session ID 附加 `require_user_session`/`AuthenticatedSession` 檢查的 `SessionUser`,並由中介軟體送出權杖;見[登入後重新產生 Session ID](#5-regenerate-the-session-id-after-login)。上面的 `/login` 則是在本文中把權杖交給 API 用戶端:`create_session(user=...)` 同樣會設定 `session.user`,但中介軟體不會為不是由它載入或建立的 Session 送出任何東西。寫入 `request.session` 的鍵(`request.session["user_id"] = ...`)是應用程式資料:函式庫不會把它視為登入,因此這種 Session 仍會讓 `AuthenticatedSession` 回應 `401`。

Expand Down Expand Up @@ -406,7 +406,7 @@ from fastapi_cachex.session.dependencies import (
OptionalSession, # Session | None
RequiredSession, # Session
SessionDep, # Session
UserSessionDep, # Session;匿名 Session 也會通過,見上文
UserSessionDep, # 自 0.4.0 起與 AuthenticatedSession 相同
AuthenticatedSession, # 帶有使用者的 Session(require_user_session)
SessionManagerDep, # SessionManager
)
Expand Down
16 changes: 13 additions & 3 deletions tests/session/test_starlette_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from fastapi_cachex.session.config import SessionConfig
from fastapi_cachex.session.dependencies import AuthenticatedSession
from fastapi_cachex.session.dependencies import RequiredSession
from fastapi_cachex.session.dependencies import UserSessionDep
from fastapi_cachex.session.dependencies import get_session
from fastapi_cachex.session.dependencies import rotate_session_id
from fastapi_cachex.session.exceptions import SessionNotFoundError
Expand Down Expand Up @@ -1105,10 +1106,19 @@ async def test_writing_after_clear_starts_a_new_anonymous_session(
assert fresh.data == {"flash": "signed out"}


@pytest.mark.parametrize(
"user_session",
[AuthenticatedSession, UserSessionDep],
ids=["AuthenticatedSession", "UserSessionDep"],
)
async def test_require_user_session_rejects_anonymous_sessions(
manager: SessionManager, config: SessionConfig
manager: SessionManager, config: SessionConfig, user_session: Any
) -> None:
"""An anonymous cart session passes get_session but not require_user_session (#114)."""
"""An anonymous cart session passes get_session but not require_user_session.

``AuthenticatedSession`` (#114) and, since 0.4.0, ``UserSessionDep`` (#127)
both require a session with a user.
"""
app = FastAPI()
app.add_middleware(FastAPICacheXSessionMiddleware, session_manager=manager)

Expand All @@ -1122,7 +1132,7 @@ async def any_session(session: RequiredSession) -> dict[str, bool]:
return {"user": session.user is not None}

@app.get("/account")
async def account(session: AuthenticatedSession) -> dict[str, str]:
async def account(session: user_session) -> dict[str, str]:
assert session.user is not None
return {"user_id": session.user.user_id}

Expand Down
Loading