Repository navigation
每次請求檢查停權:入口的 cookie 檢查、run 委派與內部身分回 403 tenant_suspended(B1) #777
Description
背景 (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 回 403tenant_suspended,與permission_denied分開。- run 委派的建立與換 token 都拒絕;
RunDelegationError、OnBehalfError原樣傳回tenant_suspended。 CurrentSession的每個租戶帶狀態;重新產生api/openapi.json與前端 API client。- 停權期間仍可用:讀目前 session、切換租戶、登出、偏好設定;平台管理員的平台端點(例如恢復租戶)在身分服務內照常可用。
- 文件:
services/identity/README.md、crates/service-kit的 README,寫明上面的代價。
不包含
- SPA 的畫面邏輯(子 issue「SPA 停權說明頁接上真實後端」)。
- 閘道器與 certs(子 issue「certs 訂閱租戶狀態」、停權與恢復的下游收斂:撤銷憑證、踢線、停止與恢復設定下發(B1) #108)。
現狀 (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)
- 父 issue:租戶狀態機與停權、恢復:記錄原因、版本化狀態與每次請求檢查(B1) #107
- 前置 issue(blocked by):身分服務的租戶狀態機:停權(記錄原因)、恢復的平台管理員 API 與版本化狀態事件(B1) #776
- 後續:同一個父 issue 下的「SPA 停權說明頁接上真實後端」
- 檔案:
services/identity/src/session.rs、services/identity/src/ext_authz.rs、services/identity/src/delegation.rs、services/identity/src/exchange.rs、crates/contract/src/identity.rs、crates/service-kit/src/identity/permission.rs、crates/service-kit/src/identity/run_delegation.rs - 文件:docs/requirements/rebuild-checklist.md B1 列;
docs/architecture/permissions.md;frontend/src/session/SessionProvider.tsx(目前只在 403tenant_suspended時顯示停權頁) - 指令:
CI=1 cargo test -p identity、cargo test -p service-kit、cargo run -p openapi、bun run --cwd frontend api:generate
Activity
Ruisi-Lu commentedon Oct 30, 2026
Implementation note (2026-10-30):
PR #797(head
3350d1bb)。做法
identity.memberships LEFT JOIN identity.tenants),在同一個租戶的 RLS 交易裡,沒有快取。suspended與deleted處理方式相同。tenant_suspended,不簽 token。tenant_suspended。Requirement::Tenant在比對角色之前,以 403tenant_suspended拒絕帶這個 claim 的 token。OwnRequest、OwnPlatformAdmin不看這個 claim,所以讀 session、切換租戶、登出、偏好設定與平台端點照常可用。OnBehalf與RunDelegations照傳tenant_suspended。CurrentSession的每個租戶帶state,不帶停權原因。api/openapi.json與前端 client 已重新產生。前端 zod schema 只讀這個欄位,畫面由 SPA 停權說明頁接上真實後端,並以瀏覽器實測(B1) #780 處理。設計決定
identity.tenants列的成員資格視為active:identity_app沒有 tenants 的 DELETE 權限;安全審查(獨立 reviewer,Opus)
沒有 Critical、High 或 Medium。module doc 表格的錯誤已在
3350d1bb修正。其餘 Low:
active,等於 fail open。現在是安全的:狀態 API 需要這一列,服務角色也刪不了租戶。但 合約終止:資料保留、匯出交付、期滿刪除與閘道器回收(B1) #109 的「期滿刪除」如果先刪租戶列,成員的 session 會被當成正常租戶放行。要求是租戶列和成員資格、session 一起刪,或比它們晚刪。另一個作法是讓種子資料都建立租戶列,之後把 NULL 當成拒絕。state是嚴格列舉。伺服器以後加了新的狀態,或滾動更新時新版 SPA 連到舊版身分服務,讀 session 都會失敗。後續提案
tenant_suspended就結束 run,或在每輪模型呼叫之間以委派檢查一次。驗證
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通過。Merge origin/main into feat/779-suspension-pending-ui