From ed4abd079d4389a9ebf862e45932663c0c8502cf Mon Sep 17 00:00:00 2001 From: allen0099 Date: Tue, 29 Sep 2026 14:53:38 +0000 Subject: [PATCH] feat(session)!: make UserSessionDep require a session with a user UserSessionDep was Depends(get_session) and admitted anonymous sessions despite its name. Resolve it through require_user_session, like AuthenticatedSession, so an anonymous session gets 401. Parametrize the anonymous-session test over both annotations and update the session docs, which described it as an alias until 0.4.0. BREAKING CHANGE: UserSessionDep requires a user; routes that should admit anonymous sessions use SessionDep. --- changelog.d/127.changed.md | 4 ++++ docs/SESSION.md | 8 +++++--- fastapi_cachex/session/dependencies.py | 5 ++--- i18n/zh-TW/docs/SESSION.md | 4 ++-- tests/session/test_starlette_middleware.py | 16 +++++++++++++--- 5 files changed, 26 insertions(+), 11 deletions(-) create mode 100644 changelog.d/127.changed.md diff --git a/changelog.d/127.changed.md b/changelog.d/127.changed.md new file mode 100644 index 0000000..d89da17 --- /dev/null +++ b/changelog.d/127.changed.md @@ -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`. diff --git a/docs/SESSION.md b/docs/SESSION.md index 782cdeb..f1f7fae 100644 --- a/docs/SESSION.md +++ b/docs/SESSION.md @@ -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 @@ -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 ) diff --git a/fastapi_cachex/session/dependencies.py b/fastapi_cachex/session/dependencies.py index 7199076..8c431c2 100644 --- a/fastapi_cachex/session/dependencies.py +++ b/fastapi_cachex/session/dependencies.py @@ -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)] diff --git a/i18n/zh-TW/docs/SESSION.md b/i18n/zh-TW/docs/SESSION.md index de8d9bf..4d56bd3 100644 --- a/i18n/zh-TW/docs/SESSION.md +++ b/i18n/zh-TW/docs/SESSION.md @@ -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`。 @@ -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 ) diff --git a/tests/session/test_starlette_middleware.py b/tests/session/test_starlette_middleware.py index ac4cf52..bbd0c03 100644 --- a/tests/session/test_starlette_middleware.py +++ b/tests/session/test_starlette_middleware.py @@ -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 @@ -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) @@ -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}