Skip to content

每次請求檢查停權:入口的 cookie 檢查、run 委派與內部身分回 403 tenant_suspended(B1) #777

Description

@Ruisi-Lu

背景 (Why)

#107(租戶停權,清單 B1)要求停權後「API 與 AI 由每次請求的 session 檢查即時擋下」。租戶狀態由 #776 定義;本 issue 讓每個請求依它擋下。

2026-10-30 使用者的決定:入口與服務兩層都擋。入口的 ext_authz 對身分服務以外的路由直接回 403 tenant_suspended、不簽內部 token;身分服務自己的端點以 token 的 claim 加共用的角色檢查擋。代價(寫進文件):平台管理員的目前租戶被停權時,要先切換租戶才能用其他服務的平台端點;已換發出去的內部 token 最多 60 秒內仍有效(現有設計)。

協調者依拆分提案的建議定下(2026-10-30):允許切換到已停權的租戶,切過去後 session 回應標出停權,SPA 顯示停權頁(#107 的子 issue「SPA 停權說明頁接上真實後端」),不回錯誤。

拆分時的發現(main e4994a25):換發出來的 token 不重讀資料庫(services/identity/src/exchange.rs),run 委派每次換 token 會重讀成員資格(services/identity/src/delegation.rs),所以要在那裡擋;crates/service-kit/src/identity/run_delegation.rs 的 RunDelegationError::code 目前只把 unauthenticated 與 permission_denied 傳回去,要加上 tenant_suspended。GET /api/identity/v1/sessions/current 的回應目前沒有租戶狀態。

範圍 (What)

包含

  • PgSessionStore 讀成員角色的同一筆租戶交易裡讀租戶狀態;deleted 一律當停權。
  • ext_authz:目標不是身分服務的請求直接回 403 tenant_suspended、不簽 token;目標是身分服務的請求,內部 token 帶新的選用 claim(例如 tenant_suspended)。
  • crates/service-kit 的 Requirement::Tenant 遇到這個 claim 回 403 tenant_suspended,與 permission_denied 分開。
  • run 委派的建立與換 token 都拒絕;RunDelegationError、OnBehalfError 原樣傳回 tenant_suspended。
  • CurrentSession 的每個租戶帶狀態;重新產生 api/openapi.json 與前端 API client。
  • 停權期間仍可用:讀目前 session、切換租戶、登出、偏好設定;平台管理員的平台端點(例如恢復租戶)在身分服務內照常可用。
  • 文件:services/identity/README.md、crates/service-kit 的 README,寫明上面的代價。

不包含

現狀 (State)

2026-10-30 完成:PR #797(head 3350d1bb)經人工核准,CI 通過,squash 合併為 774d24c2。驗收條件 1–4 都有測試與檢查的證據(見 PR 的「驗收條件逐條對照」)。沒有新增 migration。設計決定、安全審查的 Low 與後續提案見 2026-10-30 的 Implementation note 留言。

驗收條件 (Acceptance Criteria)

  • 停權後同一 session 的下一個請求回 403 tenant_suspended(測試),涵蓋:別的服務的路由、身分服務自己的租戶範圍端點、AI 的 run 委派換 token
  • 停權期間讀目前 session、切換租戶、登出、偏好設定照常可用;平台管理員的目前租戶被停權時仍能恢復租戶(測試)
  • 恢復後下一個請求恢復正常(測試)
  • cargo run -p openapi -- --check、bun run --cwd frontend api:check、CI=1 cargo test -p identity、cargo test -p service-kit、cargo clippy -p service-kit --no-default-features --all-targets -- -D warnings、cargo clippy --workspace --all-targets -- -D warnings 通過

指標 (Pointers)

Activity

  1. added theissue type on Oct 30, 2026
  2. added
    priority:P2backlog,尚可等待
    security安全性問題或加固
    area:identity身分與租戶服務:services/identity
    on Oct 30, 2026
  3. self-assigned this
    on Oct 30, 2026
  4. Ruisi-Lu commented on Oct 30, 2026

    @Ruisi-Lu
    MemberAuthor

    Implementation note (2026-10-30):

    PR #797(head 3350d1bb)。

    做法

    • 身分服務的 ext_authz 以一個查詢讀成員角色與租戶狀態(identity.memberships LEFT JOIN identity.tenants),在同一個租戶的 RLS 交易裡,沒有快取。suspended 與 deleted 處理方式相同。
    • 路由到身分服務以外的服務:回 403 tenant_suspended,不簽 token。
    • 路由到身分服務:照常簽 token,token 帶選填的 claim tenant_suspended。
      • service-kit 的 Requirement::Tenant 在比對角色之前,以 403 tenant_suspended 拒絕帶這個 claim 的 token。
      • OwnRequest、OwnPlatformAdmin 不看這個 claim,所以讀 session、切換租戶、登出、偏好設定與平台端點照常可用。
    • token 交換拒絕帶這個 claim 的 token(第二道防線)。
    • run 委派:
      • 建立時看 claim,也看當下的狀態;
      • 每次換 token 都重查狀態;
      • 恢復之後,同一個委派又換得到 token。
    • OnBehalf 與 RunDelegations 照傳 tenant_suspended。
    • CurrentSession 的每個租戶帶 state,不帶停權原因。api/openapi.json 與前端 client 已重新產生。前端 zod schema 只讀這個欄位,畫面由 SPA 停權說明頁接上真實後端,並以瀏覽器實測(B1) #780 處理。
    • 沒有新的 migration。

    設計決定

    • 沒有 identity.tenants 列的成員資格視為 active:
      • 停權需要這一列,狀態 API 對沒有列的租戶回 404;
      • identity_app 沒有 tenants 的 DELETE 權限;
      • 很多測試與種子資料只寫成員資格。
    • 資料庫出現這版不認得的狀態時回 503(fail closed),ext_authz 與目前 session 的 API 都是。
    • 已簽出的內部 token 在剩下的效期內(最多 60 秒)仍有效,也仍換得到 token 交換的 token(現有設計,已寫進 README)。

    安全審查(獨立 reviewer,Opus)

    沒有 Critical、High 或 Medium。module doc 表格的錯誤已在 3350d1bb 修正。

    其餘 Low:

    • L1:沒有租戶列時視為 active,等於 fail open。現在是安全的:狀態 API 需要這一列,服務角色也刪不了租戶。但 合約終止:資料保留、匯出交付、期滿刪除與閘道器回收(B1) #109 的「期滿刪除」如果先刪租戶列,成員的 session 會被當成正常租戶放行。要求是租戶列和成員資格、session 一起刪,或比它們晚刪。另一個作法是讓種子資料都建立租戶列,之後把 NULL 當成拒絕。
    • L2:assistant 進行中的 run 對 AI 閘道的模型呼叫不經停權檢查。工具呼叫在下一次換 token 就被擋下,但模型呼叫最多可能持續到委派到期(900 秒)。清單 B1 寫的是「AI 即時擋下」。
    • L3:平台管理員的目前租戶被停權時,其他服務(certs、assets 閘道器)的平台端點也被擋下,要先切換租戶。只屬於這個租戶的平台管理員無處可切,要等恢復。這是本 issue 要求記錄的代價,已寫進 README。
    • L4:前端的 state 是嚴格列舉。伺服器以後加了新的狀態,或滾動更新時新版 SPA 連到舊版身分服務,讀 session 都會失敗。

    後續提案

    驗證

    • 建置主機跑了 CI=1 cargo test --locked --workspace --exclude agent:2034 passed、0 failed、1 ignored。
    • 在同一次執行通過的還有 services/identity/tests/tenant_suspension.rs 的兩個端到端測試。
    • cargo fmt --check、cargo run -p openapi -- --check、cargo clippy --workspace --all-targets -- -D warnings、service-kit 不含預設 feature 的 clippy 與測試也都通過。
    • 前端:api:check、typecheck、lint、test、build、check:dist 通過。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:identity身分與租戶服務:services/identitypriority:P2backlog,尚可等待security安全性問題或加固

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions